disable file input while awaiting publish
add spinner to publishes in sidebar
add spinner and Publishing title on Publish page
add WebUploadList to Publishes
add WebUploadItem
 - thumb
 - name
 - progress bar
 - abort button
beforeunload prevent closing tab / navigation
enforce and notify about publish size limit
6 outstanding flow complaints
This commit is contained in:
jessop 2019-10-10 20:37:18 -04:00 committed by Sean Yesmunt
parent dc1863890e
commit 3cc69ddaf0
22 changed files with 260 additions and 28 deletions

View file

@ -1,4 +1,6 @@
declare type WebFile = { declare type WebFile = {
name: string, name: string,
title?: string,
path?: string, path?: string,
size?: string,
} }

View file

@ -1,8 +1,8 @@
import { HEADERS, Lbry } from 'lbry-redux'; import { Lbry } from 'lbry-redux';
import apiPublishCallViaWeb from './publish'; import apiPublishCallViaWeb from './publish';
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
export const SDK_API_URL = process.env.SDK_API_URL || 'https://api.lbry.tv/api/v1/proxy'; export const SDK_API_URL = process.env.SDK_API_URL || 'https://api.lbry.tv/api/v1/proxy';
Lbry.setDaemonConnectionString(SDK_API_URL); Lbry.setDaemonConnectionString(SDK_API_URL);
Lbry.setOverride( Lbry.setOverride(
@ -11,8 +11,8 @@ Lbry.setOverride(
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
apiPublishCallViaWeb( apiPublishCallViaWeb(
SDK_API_URL, SDK_API_URL,
Lbry.getApiRequestHeaders() && Object.keys(Lbry.getApiRequestHeaders()).includes(HEADERS.AUTH_TOKEN) Lbry.getApiRequestHeaders() && Object.keys(Lbry.getApiRequestHeaders()).includes(X_LBRY_AUTH_TOKEN)
? Lbry.getApiRequestHeaders()[HEADERS.AUTH_TOKEN] ? Lbry.getApiRequestHeaders()[X_LBRY_AUTH_TOKEN]
: '', : '',
'publish', 'publish',
params, params,

View file

@ -1,5 +1,13 @@
// @flow // @flow
import { HEADERS } from 'lbry-redux'; /*
https://api.lbry.tv/api/v1/proxy currently expects publish to consist
of a multipart/form-data POST request with:
- 'file' binary
- 'json_payload' collection of publish params to be passed to the server's sdk.
*/
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
import { doUpdateUploadProgress } from 'lbryinc';
// A modified version of Lbry.apiCall that allows // A modified version of Lbry.apiCall that allows
// to perform calling methods at arbitrary urls // to perform calling methods at arbitrary urls
// and pass form file fields // and pass form file fields
@ -26,27 +34,34 @@ export default function apiPublishCallViaWeb(
body.append('file', fileField); body.append('file', fileField);
body.append('json_payload', jsonPayload); body.append('json_payload', jsonPayload);
function makeRequest(connectionString, method, token, body) { function makeRequest(connectionString, method, token, body, params) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
xhr.open(method, connectionString); xhr.open(method, connectionString);
xhr.setRequestHeader(HEADERS.AUTH_TOKEN, token); xhr.setRequestHeader(X_LBRY_AUTH_TOKEN, token);
xhr.responseType = 'json'; xhr.responseType = 'json';
xhr.upload.onprogress = e => { xhr.upload.onprogress = e => {
let percentComplete = Math.ceil((e.loaded / e.total) * 100); let percentComplete = Math.ceil((e.loaded / e.total) * 100);
console.log(percentComplete); // put your upload state update here window.store.dispatch(doUpdateUploadProgress(percentComplete, params, xhr));
}; };
xhr.onload = () => { xhr.onload = () => {
window.store.dispatch(doUpdateUploadProgress(undefined, params));
resolve(xhr); resolve(xhr);
}; };
xhr.onerror = () => { xhr.onerror = () => {
reject({ status: xhr.status, statusText: xhr.statusText }); window.store.dispatch(doUpdateUploadProgress(undefined, params));
reject(new Error(__('There was a problem with your upload')));
};
xhr.onabort = () => {
window.store.dispatch(doUpdateUploadProgress(undefined, params));
reject(new Error(__('You aborted your publish upload')));
}; };
xhr.send(body); xhr.send(body);
}); });
} }
return makeRequest(connectionString, 'POST', token, body) return makeRequest(connectionString, 'POST', token, body, params)
.then(xhr => { .then(xhr => {
let error; let error;
if (xhr) { if (xhr) {
@ -55,7 +70,7 @@ export default function apiPublishCallViaWeb(
} else if (xhr.statusText) { } else if (xhr.statusText) {
error = new Error(xhr.statusText); error = new Error(xhr.statusText);
} else { } else {
error = new Error('Upload likely timed out. Try a smaller file while we work on this.'); error = new Error(__('Upload likely timed out. Try a smaller file while we work on this.'));
} }
} }

View file

@ -8,6 +8,7 @@ import {
doFetchAccessToken, doFetchAccessToken,
selectAccessToken, selectAccessToken,
selectGetSyncErrorMessage, selectGetSyncErrorMessage,
selectUploadCount,
} from 'lbryinc'; } from 'lbryinc';
import { doFetchTransactions, doFetchChannelListMine, selectBalance } from 'lbry-redux'; import { doFetchTransactions, doFetchChannelListMine, selectBalance } from 'lbry-redux';
import { makeSelectClientSetting, selectThemePath } from 'redux/selectors/settings'; import { makeSelectClientSetting, selectThemePath } from 'redux/selectors/settings';
@ -26,6 +27,7 @@ const select = state => ({
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state), syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
syncError: selectGetSyncErrorMessage(state), syncError: selectGetSyncErrorMessage(state),
accessToken: selectAccessToken(state), accessToken: selectAccessToken(state),
uploadCount: selectUploadCount(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -47,6 +47,7 @@ type Props = {
checkSync: () => void, checkSync: () => void,
setSyncEnabled: boolean => void, setSyncEnabled: boolean => void,
syncEnabled: boolean, syncEnabled: boolean,
uploadCount: number,
balance: ?number, balance: ?number,
accessToken: ?string, accessToken: ?string,
syncError: ?string, syncError: ?string,
@ -68,6 +69,7 @@ function App(props: Props) {
setSyncEnabled, setSyncEnabled,
syncEnabled, syncEnabled,
checkSync, checkSync,
uploadCount,
balance, balance,
accessToken, accessToken,
history, history,
@ -128,6 +130,16 @@ function App(props: Props) {
}); });
}, [balance, accessToken, hasDeterminedIfNewUser, setHasDeterminedIfNewUser]); }, [balance, accessToken, hasDeterminedIfNewUser, setHasDeterminedIfNewUser]);
useEffect(() => {
if (!uploadCount) return;
const handleBeforeUnload = event => {
event.preventDefault();
event.returnValue = 'magic';
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [uploadCount]);
useEffect(() => { useEffect(() => {
ReactModal.setAppElement(appRef.current); ReactModal.setAppElement(appRef.current);
fetchAccessToken(); fetchAccessToken();

View file

@ -13,6 +13,8 @@ type Props = {
fileLabel?: string, fileLabel?: string,
directoryLabel?: string, directoryLabel?: string,
accept?: string, accept?: string,
error?: string,
disabled?: boolean,
}; };
class FileSelector extends React.PureComponent<Props> { class FileSelector extends React.PureComponent<Props> {
@ -48,7 +50,7 @@ class FileSelector extends React.PureComponent<Props> {
input: ?HTMLInputElement; input: ?HTMLInputElement;
render() { render() {
const { type, currentPath, label, fileLabel, directoryLabel, placeholder, accept } = this.props; const { type, currentPath, label, fileLabel, directoryLabel, placeholder, accept, error, disabled } = this.props;
const buttonLabel = type === 'file' ? fileLabel || __('Choose File') : directoryLabel || __('Choose Directory'); const buttonLabel = type === 'file' ? fileLabel || __('Choose File') : directoryLabel || __('Choose Directory');
const placeHolder = currentPath || placeholder; const placeHolder = currentPath || placeholder;
@ -58,10 +60,14 @@ class FileSelector extends React.PureComponent<Props> {
label={label} label={label}
webkitdirectory="true" webkitdirectory="true"
className="form-field--copyable" className="form-field--copyable"
error={error}
disabled={disabled}
type="text" type="text"
readOnly="readonly" readOnly="readonly"
value={placeHolder || __('Choose a file')} value={placeHolder || __('Choose a file')}
inputButton={<Button button="primary" onClick={this.fileInputButton} label={buttonLabel} />} inputButton={
<Button button="primary" disabled={disabled} onClick={this.fileInputButton} label={buttonLabel} />
}
/> />
<input <input
type={'file'} type={'file'}

View file

@ -8,6 +8,7 @@ const select = state => ({
filePath: makeSelectPublishFormValue('filePath')(state), filePath: makeSelectPublishFormValue('filePath')(state),
isStillEditing: selectIsStillEditing(state), isStillEditing: selectIsStillEditing(state),
balance: selectBalance(state), balance: selectBalance(state),
publishing: makeSelectPublishFormValue('publishing')(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -1,10 +1,11 @@
// @flow // @flow
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React from 'react'; import React, { useState } from 'react';
import { regexInvalidURI } from 'lbry-redux'; import { regexInvalidURI } from 'lbry-redux';
import FileSelector from 'component/common/file-selector'; import FileSelector from 'component/common/file-selector';
import Button from 'component/button'; import Button from 'component/button';
import Card from 'component/common/card'; import Card from 'component/common/card';
import Spinner from 'component/spinner';
type Props = { type Props = {
name: ?string, name: ?string,
@ -13,10 +14,14 @@ type Props = {
balance: number, balance: number,
updatePublishForm: ({}) => void, updatePublishForm: ({}) => void,
disabled: boolean, disabled: boolean,
publishing: boolean,
}; };
function PublishFile(props: Props) { function PublishFile(props: Props) {
const { name, balance, filePath, isStillEditing, updatePublishForm, disabled } = props; const { name, balance, filePath, isStillEditing, updatePublishForm, disabled, publishing } = props;
// This is basically for displaying the 500mb limit
const [fileError, setFileError] = useState('');
let currentFile = ''; let currentFile = '';
if (filePath) { if (filePath) {
@ -30,29 +35,57 @@ function PublishFile(props: Props) {
function handleFileChange(file: WebFile) { function handleFileChange(file: WebFile) {
// if electron, we'll set filePath to the path string because SDK is handling publishing. // if electron, we'll set filePath to the path string because SDK is handling publishing.
// if web, we set the filePath (dumb name) to the File() object // if web, we set the filePath (dumb name) to the File() object
// file.path will be undefined from web due to browser security, so it will default to the File Object. // File.path will be undefined from web due to browser security, so it will default to the File Object.
// @if TARGET='web'
// we only need to enforce file sizes on 'web'
const PUBLISH_SIZE_LIMIT: number = 512000000;
if (typeof file !== 'string') {
if (file && file.size && Number(file.size) > PUBLISH_SIZE_LIMIT) {
setFileError('File uploads currently limited to 500MB. Download the app for unlimited publishing.');
updatePublishForm({ filePath: '', name: '' });
return;
} else {
setFileError('');
}
}
// @endif
const publishFormParams: { filePath: string | WebFile, name?: string } = { const publishFormParams: { filePath: string | WebFile, name?: string } = {
filePath: file.path || file, filePath: file.path || file,
name: file.name, name: file.name,
}; };
const parsedFileName = file.name.replace(regexInvalidURI, ''); const parsedFileName = file.name.replace(regexInvalidURI, '');
publishFormParams.name = parsedFileName.replace(' ', '-'); publishFormParams.name = parsedFileName.replace(' ', '-');
updatePublishForm(publishFormParams); updatePublishForm(publishFormParams);
} }
let title;
if (publishing) {
title = (
<span>
{__('Publishing')}
<Spinner type={'small'} />
</span>
);
} else {
title = isStillEditing ? __('Edit') : __('Publish');
}
return ( return (
<Card <Card
icon={ICONS.PUBLISH} icon={ICONS.PUBLISH}
disabled={disabled || balance === 0} disabled={disabled || balance === 0}
title={isStillEditing ? __('Edit') : __('Publish')} title={title}
subtitle={ subtitle={
isStillEditing ? __('You are currently editing a claim.') : __('Publish something totally wacky and wild.') isStillEditing ? __('You are currently editing a claim.') : __('Publish something totally wacky and wild.')
} }
actions={ actions={
<React.Fragment> <React.Fragment>
<FileSelector currentPath={currentFile} onFileChosen={handleFileChange} /> <FileSelector
disabled={disabled}
currentPath={currentFile}
onFileChosen={handleFileChange}
error={fileError}
/>
{!isStillEditing && ( {!isStillEditing && (
<p className="help"> <p className="help">
{__('For video content, use MP4s in H264/AAC format for best compatibility.')}{' '} {__('For video content, use MP4s in H264/AAC format for best compatibility.')}{' '}

View file

@ -1,4 +1,12 @@
// @flow // @flow
/*
On submit, this component calls publish, which dispatches doPublishDesktop.
doPublishDesktop calls lbry-redux Lbry publish method using lbry-redux publish state as params.
Publish simply instructs the SDK to find the file path on disk and publish it with the provided metadata.
On web, the Lbry publish method call is overridden in platform/web/api-setup, using a function in platform/web/publish.
File upload is carried out in the background by that function.
*/
import React, { useEffect, Fragment } from 'react'; import React, { useEffect, Fragment } from 'react';
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim'; import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux'; import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux';
@ -122,7 +130,7 @@ function PublishForm(props: Props) {
return ( return (
<Fragment> <Fragment>
<PublishFile disabled={formDisabled}/> <PublishFile disabled={publishing} />
<div className={classnames({ 'card--disabled': formDisabled })}> <div className={classnames({ 'card--disabled': formDisabled })}>
<PublishText disabled={formDisabled} /> <PublishText disabled={formDisabled} />
<Card actions={<SelectThumbnail />} /> <Card actions={<SelectThumbnail />} />

View file

@ -24,7 +24,7 @@ function SelectAsset(props: Props) {
const { onUpdate, assetName, currentValue, recommended } = props; const { onUpdate, assetName, currentValue, recommended } = props;
const [assetSource, setAssetSource] = useState(SOURCE_URL); const [assetSource, setAssetSource] = useState(SOURCE_URL);
const [pathSelected, setPathSelected] = useState(''); const [pathSelected, setPathSelected] = useState('');
const [fileSelected, setFileSelected] = useState(null); const [fileSelected, setFileSelected] = useState('');
const [uploadStatus, setUploadStatus] = useState(SPEECH_READY); const [uploadStatus, setUploadStatus] = useState(SPEECH_READY);
function doUploadAsset(file) { function doUploadAsset(file) {
@ -95,7 +95,7 @@ function SelectAsset(props: Props) {
button={'secondary'} button={'secondary'}
onClick={() => { onClick={() => {
setPathSelected(''); setPathSelected('');
setFileSelected(null); setFileSelected('');
}} }}
> >
Clear Clear

View file

@ -2,7 +2,7 @@ import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { selectFollowedTags } from 'lbry-redux'; import { selectFollowedTags } from 'lbry-redux';
import { selectUserEmail } from 'lbryinc'; import { selectUserEmail, selectUploadCount } from 'lbryinc';
import SideBar from './view'; import SideBar from './view';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
@ -11,6 +11,7 @@ const select = state => ({
followedTags: selectFollowedTags(state), followedTags: selectFollowedTags(state),
language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change
email: selectUserEmail(state), email: selectUserEmail(state),
uploadCount: selectUploadCount(state),
}); });
const perform = () => ({}); const perform = () => ({});

View file

@ -6,16 +6,18 @@ import Button from 'component/button';
import Tag from 'component/tag'; import Tag from 'component/tag';
import StickyBox from 'react-sticky-box/dist/esnext'; import StickyBox from 'react-sticky-box/dist/esnext';
import 'css-doodle'; import 'css-doodle';
import Spinner from 'component/spinner';
type Props = { type Props = {
subscriptions: Array<Subscription>, subscriptions: Array<Subscription>,
followedTags: Array<Tag>, followedTags: Array<Tag>,
email: ?string, email: ?string,
obscureSideBar: boolean, obscureSideBar: boolean,
uploadCount: number,
}; };
function SideBar(props: Props) { function SideBar(props: Props) {
const { subscriptions, followedTags, obscureSideBar } = props; const { subscriptions, followedTags, obscureSideBar, uploadCount } = props;
function buildLink(path, label, icon, guide) { function buildLink(path, label, icon, guide) {
return { return {
navigate: path ? `$/${path}` : '/', navigate: path ? `$/${path}` : '/',
@ -52,7 +54,18 @@ function SideBar(props: Props) {
...buildLink(PAGES.CHANNELS, __('Channels'), ICONS.CHANNEL), ...buildLink(PAGES.CHANNELS, __('Channels'), ICONS.CHANNEL),
}, },
{ {
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISH), ...buildLink(
PAGES.PUBLISHED,
uploadCount ? (
<span>
{__('Publishes')}
<Spinner type="small" />
</span>
) : (
__('Publishes')
),
ICONS.PUBLISH
),
}, },
].map(linkProps => ( ].map(linkProps => (
<li key={linkProps.label}> <li key={linkProps.label}>

View file

@ -0,0 +1,13 @@
import { connect } from 'react-redux';
import { selectCurrentUploads, selectUploadCount } from 'lbryinc';
import WebUploadList from './view';
const select = state => ({
currentUploads: selectCurrentUploads(state),
uploadCount: selectUploadCount(state),
});
export default connect(
select,
null
)(WebUploadList);

View file

@ -0,0 +1,41 @@
// @flow
import React from 'react';
import Button from 'component/button';
import CardMedia from 'component/cardMedia';
type Props = {
params: UpdatePublishFormData,
progress: string,
xhr?: () => void,
};
export default function WebUploadItem(props: Props) {
const { params, progress, xhr } = props;
return (
<li className={'claim-preview'}>
<CardMedia thumbnail={params.thumbnail_url} />
<div className={'claim-preview-metadata'}>
<div className="claim-preview-info">
<div className="claim-preview-title">{params.title}</div>
{xhr && (
<div className="card__actions--inline">
<Button
button={'primary'}
onClick={() => {
xhr.abort();
}}
label={'abort'}
/>
</div>
)}
</div>
<h2>{params.name}</h2>
<div className={'claim-upload__progress--outer'}>
<div className={'claim-upload__progress--inner'} style={{ width: `${progress}%` }}>
Uploading...
</div>
</div>
</div>
</li>
);
}

View file

@ -0,0 +1,37 @@
// @flow
import * as React from 'react';
import Card from 'component/common/card';
import WebUploadItem from './internal/web-upload-item';
export type UploadItem = {
progess: string,
params: UpdatePublishFormData,
xhr?: { abort: () => void },
};
type Props = {
currentUploads: { [key: string]: UploadItem },
uploadCount: ?number,
};
export default function WebUploadList(props: Props) {
const { currentUploads, uploadCount } = props;
return (
!!uploadCount && (
<div>
<Card
title={__('Currently Uploading')}
subtitle={<span>{__('You are currently uploading one or more files for publish.')}</span>}
body={
<section>
{Object.values(currentUploads).map(({ progress, params, xhr }) => (
<WebUploadItem key={`upload${params.name}`} progress={progress} params={params} xhr={xhr} />
))}
</section>
}
/>
</div>
)
);
}

View file

@ -0,0 +1 @@
export const X_LBRY_AUTH_TOKEN = 'X-Lbry-Auth-Token';

View file

@ -27,6 +27,12 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
navigate('/$/published'); navigate('/$/published');
closeModal(); closeModal();
}} }}
confirmButtonLabel={'Show me!'}
abortButtonLabel={'Thanks!'}
onAborted={() => {
clearPublish();
closeModal();
}}
> >
<p>{__(`Your ${publishMessage} published to LBRY at the address`)}</p> <p>{__(`Your ${publishMessage} published to LBRY at the address`)}</p>
<blockquote>{uri}</blockquote> <blockquote>{uri}</blockquote>

View file

@ -5,6 +5,7 @@ import ClaimList from 'component/claimList';
import Page from 'component/page'; import Page from 'component/page';
import Paginate from 'component/common/paginate'; import Paginate from 'component/common/paginate';
import { PAGE_SIZE } from 'constants/claim'; import { PAGE_SIZE } from 'constants/claim';
import WebUploadList from 'component/webUploadList';
type Props = { type Props = {
checkPendingPublishes: () => void, checkPendingPublishes: () => void,
@ -25,6 +26,7 @@ function FileListPublished(props: Props) {
return ( return (
<Page notContained> <Page notContained>
<WebUploadList />
{urls && urls.length ? ( {urls && urls.length ? (
<div className="card"> <div className="card">
<ClaimList <ClaimList

View file

@ -21,6 +21,7 @@ import {
homepageReducer, homepageReducer,
statsReducer, statsReducer,
syncReducer, syncReducer,
lbrytvReducer,
} from 'lbryinc'; } from 'lbryinc';
import appReducer from 'redux/reducers/app'; import appReducer from 'redux/reducers/app';
import availabilityReducer from 'redux/reducers/availability'; import availabilityReducer from 'redux/reducers/availability';
@ -54,4 +55,5 @@ export default history =>
user: userReducer, user: userReducer,
wallet: walletReducer, wallet: walletReducer,
sync: syncReducer, sync: syncReducer,
lbrytv: lbrytvReducer,
}); });

View file

@ -7,6 +7,8 @@ import path from 'path';
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as MODALS from 'constants/modal_types'; import * as MODALS from 'constants/modal_types';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
import * as SETTINGS from 'constants/settings';
import { import {
Lbry, Lbry,
doBalanceSubscribe, doBalanceSubscribe,
@ -437,8 +439,9 @@ export function doAnalyticsView(uri, timeToStart) {
export function doSignIn() { export function doSignIn() {
return (dispatch, getState) => { return (dispatch, getState) => {
// @if TARGET='web' // @if TARGET='web'
const authToken = getAuthToken(); const { auth_token: authToken } = cookie.parse(document.cookie);
Lbry.setApiHeader(HEADERS.AUTH_TOKEN, authToken); Lbry.setApiHeader(X_LBRY_AUTH_TOKEN, authToken);
dispatch(doBalanceSubscribe()); dispatch(doBalanceSubscribe());
dispatch(doFetchChannelListMine()); dispatch(doFetchChannelListMine());
// @endif // @endif

View file

@ -198,3 +198,36 @@ $border-color--dark: var(--dm-color-04);
flex: 1; flex: 1;
font-size: var(--font-subtext); font-size: var(--font-subtext);
} }
.claim-upload {
flex: 1;
display: flex;
position: relative;
overflow: visible;
padding: var(--spacing-medium);
.media__thumb {
width: var(--file-list-thumbnail-width);
flex-shrink: 0;
margin-right: var(--spacing-medium);
}
[data-mode='dark'] & {
color: $lbry-white;
border-color: $border-color--dark;
}
}
.claim-upload__progress--outer {
width: 100%;
}
.claim-upload__progress--inner {
background: $lbry-teal-1;
color: $lbry-gray-1;
[data-mode='dark'] & {
background: $lbry-teal-4;
color: $lbry-white;
}
}

View file

@ -728,6 +728,7 @@
"Your LBRY credits are controllable by you and only you, via a wallet file stored locally on your computer.": "Your LBRY credits are controllable by you and only you, via a wallet file stored locally on your computer.", "Your LBRY credits are controllable by you and only you, via a wallet file stored locally on your computer.": "Your LBRY credits are controllable by you and only you, via a wallet file stored locally on your computer.",
"However, it is easy to back up manually. To backup your wallet, make a copy of the folder listed below:": "However, it is easy to back up manually. To backup your wallet, make a copy of the folder listed below:", "However, it is easy to back up manually. To backup your wallet, make a copy of the folder listed below:": "However, it is easy to back up manually. To backup your wallet, make a copy of the folder listed below:",
"Access to these files are equivalent to having access to your credits. Keep any copies you make of your wallet in a secure place. For more details on backing up and best practices %helpLink%.": "Access to these files are equivalent to having access to your credits. Keep any copies you make of your wallet in a secure place. For more details on backing up and best practices %helpLink%.", "Access to these files are equivalent to having access to your credits. Keep any copies you make of your wallet in a secure place. For more details on backing up and best practices %helpLink%.": "Access to these files are equivalent to having access to your credits. Keep any copies you make of your wallet in a secure place. For more details on backing up and best practices %helpLink%.",
}
"Your Channels": "Your Channels", "Your Channels": "Your Channels",
"Add Tags": "Add Tags", "Add Tags": "Add Tags",
"Available Balance": "Available Balance", "Available Balance": "Available Balance",