add hosting to first run (#7598)
* add hosting to first run, enable auto hosting * take welcomeVersion out of sync * app strings fix * recommended view hosting limit * small changes * fixes * appstrings * small fix
This commit is contained in:
parent
743c75df16
commit
99ceaadf8b
37 changed files with 1044 additions and 254 deletions
|
@ -16,7 +16,7 @@ COMMENT_SERVER_NAME=Odysee
|
|||
SEARCH_SERVER_API=https://lighthouse.odysee.com/search
|
||||
SOCKETY_SERVER_API=wss://sockety.odysee.com/ws
|
||||
THUMBNAIL_CDN_URL=https://image-processor.vanwanet.com/optimize/
|
||||
WELCOME_VERSION=1.1
|
||||
WELCOME_VERSION=1.2
|
||||
|
||||
# STRIPE
|
||||
# STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo'
|
||||
|
|
|
@ -2206,13 +2206,6 @@
|
|||
"Enabling a minimum amount to comment will force all comments to have tips associated with them. This can help prevent spam.": "Enabling a minimum amount to comment will force all comments to have tips associated with them. This can help prevent spam.",
|
||||
"Comments containing these words will be blocked.": "Comments containing these words will be blocked.",
|
||||
"Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8": "Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8",
|
||||
"Disk Space": "Disk Space",
|
||||
"Data Hosting": "Data Hosting",
|
||||
"Limit": "Limit",
|
||||
"Limit Space Used": "Limit Space Used",
|
||||
"Apply": "Apply",
|
||||
"Limit in GB": "Limit in GB",
|
||||
"If you set a limit, playing videos may exceed your limit until cleanup runs every 30 minutes.": "If you set a limit, playing videos may exceed your limit until cleanup runs every 30 minutes.",
|
||||
"Enable Data Hosting": "Enable Data Hosting",
|
||||
"Data over the limit will be deleted within 30 minutes. This will make the Yrbl cry a little bit.": "Data over the limit will be deleted within 30 minutes. This will make the Yrbl cry a little bit.",
|
||||
"Choose %asset%": "Choose %asset%",
|
||||
|
@ -2228,13 +2221,6 @@
|
|||
"Enable Prerelease Updates": "Enable Prerelease Updates",
|
||||
"Enable Upgrade to Test Builds": "Enable Upgrade to Test Builds",
|
||||
"Prereleases may break things and we may not be able to fix them for you.": "Prereleases may break things and we may not be able to fix them for you.",
|
||||
"Limit (GB)": "Limit (GB)",
|
||||
"Limit Hosting for Content you Use": "Limit Hosting for Content you Use",
|
||||
"Allow (GB)": "Allow (GB)",
|
||||
"Content Data Hosting helps to seed things that you watch and download.": "Content Data Hosting helps to seed things that you watch and download.",
|
||||
"Network Data Hosting allows the p2p network to store blobs unrelated to your browsing.": "Network Data Hosting allows the p2p network to store blobs unrelated to your browsing.",
|
||||
"Content: Limit (GB)": "Content: Limit (GB)",
|
||||
"Network: Allow (GB)": "Network: Allow (GB)",
|
||||
"A channel is required to repost on LBRY": "A channel is required to repost on LBRY",
|
||||
"Admin": "Admin",
|
||||
"Stickers": "Stickers",
|
||||
|
@ -2256,19 +2242,6 @@
|
|||
"Move Up": "Move Up",
|
||||
"Move Down": "Move Down",
|
||||
"Trending for #Game": "Trending for #Game",
|
||||
"Help the P2P data network by hosting data.": "Help the P2P data network by hosting data.",
|
||||
"History hosting lets you choose how much storage to use helping content you've consumed.": "History hosting lets you choose how much storage to use helping content you've consumed.",
|
||||
"Automatic hosting lets you delegate some amount of storage for the network to automatically download ad host.": "Automatic hosting lets you delegate some amount of storage for the network to automatically download ad host.",
|
||||
"Playing videos may exceed your history hosting limit until cleanup runs every 30 minutes.": "Playing videos may exceed your history hosting limit until cleanup runs every 30 minutes.",
|
||||
"History: Limit (GB)": "History: Limit (GB)",
|
||||
"Automatic: Allow (GB)": "Automatic: Allow (GB)",
|
||||
"Automatic hosting lets you delegate some amount of storage for the network to automatically download and host.": "Automatic hosting lets you delegate some amount of storage for the network to automatically download and host.",
|
||||
"History Hosting": "History Hosting",
|
||||
"Automatic Hosting": "Automatic Hosting",
|
||||
"History Hosting lets you choose how much storage to use helping content you've consumed.": "History Hosting lets you choose how much storage to use helping content you've consumed.",
|
||||
"Automatic Hosting lets you delegate some amount of storage for the network to automatically download and host.": "Automatic Hosting lets you delegate some amount of storage for the network to automatically download and host.",
|
||||
"Help improve the P2P data network (and make LBRY happy) by hosting data.": "Help improve the P2P data network (and make LBRY happy) by hosting data.",
|
||||
"Limit Hosting of Content History": "Limit Hosting of Content History",
|
||||
"Remove custom comment server": "Remove custom comment server",
|
||||
"Use Https": "Use Https",
|
||||
"Server URL": "Server URL",
|
||||
|
@ -2315,8 +2288,27 @@
|
|||
"Clear Views": "Clear Views",
|
||||
"Show Video View Progress": "Show Video View Progress",
|
||||
"Display view progress on thumbnail. This setting will not hide any blockchain activity or downloads.": "Display view progress on thumbnail. This setting will not hide any blockchain activity or downloads.",
|
||||
"%anonymous%": "%anonymous%",
|
||||
"Anon --[used in <%anonymous% Reposted>]--": "Anon",
|
||||
"This will be visible in a few minutes after you submit this form.": "This will be visible in a few minutes after you submit this form.",
|
||||
"Content Hosting": "Content Hosting",
|
||||
"Hosting": "Hosting",
|
||||
"Viewed Hosting": "Viewed Hosting",
|
||||
"Auto Hosting": "Auto Hosting",
|
||||
"Help creators and improve the P2P data network by hosting content.": "Help creators and improve the P2P data network by hosting content.",
|
||||
"I'm happy with my settings": "I'm happy with my settings",
|
||||
"We've noticed you already have some settings.": "We've noticed you already have some settings.",
|
||||
"You choose how much data to host.": "You choose how much data to host.",
|
||||
"Go back": "Go back",
|
||||
"Custom Hosting": "Custom Hosting",
|
||||
"Automatic Hosting (GB)": "Automatic Hosting (GB)",
|
||||
"* Note that as peer-to-peer software, your IP address and potentially other system information can be sent to other users, though this information is not stored permanently.": "* Note that as peer-to-peer software, your IP address and potentially other system information can be sent to other users, though this information is not stored permanently.",
|
||||
"Help improve the P2P data network (and make LBRY users happy) by hosting data.": "Help improve the P2P data network (and make LBRY users happy) by hosting data.",
|
||||
"View History Hosting lets you choose how much storage to use hosting content you've consumed.": "View History Hosting lets you choose how much storage to use hosting content you've consumed.",
|
||||
"Automatic Hosting downloads a small portion of content active on the network.": "Automatic Hosting downloads a small portion of content active on the network.",
|
||||
"Publishes --[legend, storage category]--": "Publishes",
|
||||
"Auto Hosting --[legend, storage category]--": "Auto Hosting",
|
||||
"View Hosting --[legend, storage category]--": "View Hosting",
|
||||
"%spaceUsed% of %limit% GB": "%spaceUsed% of %limit% GB",
|
||||
"%spaceUsed% of %limit% Free GB": "%spaceUsed% of %limit% Free GB",
|
||||
"Disabled": "Disabled",
|
||||
"Free --[legend, unused disk space]--": "Free",
|
||||
"--end--": "--end--"
|
||||
}
|
||||
|
|
21
ui/component/appStorageVisualization/index.js
Normal file
21
ui/component/appStorageVisualization/index.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { connect } from 'react-redux';
|
||||
import StorageViz from './view';
|
||||
import {
|
||||
selectViewBlobSpace,
|
||||
selectViewHostingLimit,
|
||||
selectAutoBlobSpace,
|
||||
selectPrivateBlobSpace,
|
||||
selectAutoHostingLimit,
|
||||
} from 'redux/selectors/settings';
|
||||
import { selectDiskSpace } from 'redux/selectors/app';
|
||||
|
||||
const select = (state) => ({
|
||||
diskSpace: selectDiskSpace(state),
|
||||
viewHostingLimit: selectViewHostingLimit(state),
|
||||
autoHostingLimit: selectAutoHostingLimit(state),
|
||||
viewBlobSpace: selectViewBlobSpace(state),
|
||||
autoBlobSpace: selectAutoBlobSpace(state),
|
||||
privateBlobSpace: selectPrivateBlobSpace(state),
|
||||
});
|
||||
|
||||
export default connect(select)(StorageViz);
|
126
ui/component/appStorageVisualization/view.jsx
Normal file
126
ui/component/appStorageVisualization/view.jsx
Normal file
|
@ -0,0 +1,126 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
|
||||
type Props = {
|
||||
// --- select ---
|
||||
diskSpace: DiskSpace, // KB
|
||||
viewHostingLimit: number, // MB
|
||||
autoHostingLimit: number,
|
||||
viewBlobSpace: number,
|
||||
autoBlobSpace: number,
|
||||
privateBlobSpace: number,
|
||||
};
|
||||
|
||||
function StorageViz(props: Props) {
|
||||
const { diskSpace, viewHostingLimit, autoHostingLimit, viewBlobSpace, autoBlobSpace, privateBlobSpace } = props;
|
||||
|
||||
if (!diskSpace || !diskSpace.total) {
|
||||
return (
|
||||
<div className={'storage__wrapper'}>
|
||||
<div className={'storage__bar'}>
|
||||
<div className="help">{__('Cannot get disk space information.')}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totalMB = diskSpace && Math.floor(Number(diskSpace.total) / 1024);
|
||||
const freeMB = diskSpace && Math.floor(Number(diskSpace.free) / 1024);
|
||||
const otherMB = totalMB - (freeMB + viewBlobSpace + autoBlobSpace + privateBlobSpace);
|
||||
const autoFree = autoHostingLimit - autoBlobSpace;
|
||||
const viewFree = viewHostingLimit > 0 ? viewHostingLimit - viewBlobSpace : freeMB - autoFree;
|
||||
const unallocFree = freeMB - viewFree - autoFree;
|
||||
const viewLimit =
|
||||
viewHostingLimit === 0
|
||||
? freeMB - (autoHostingLimit - autoBlobSpace) + viewBlobSpace
|
||||
: viewHostingLimit + viewBlobSpace;
|
||||
|
||||
const getPercent = (val, lim = totalMB) => (val / lim) * 100;
|
||||
const getGB = (val) => (Number(val) / 1024).toFixed(2);
|
||||
|
||||
const otherPercent = getPercent(otherMB);
|
||||
const privatePercent = getPercent(privateBlobSpace);
|
||||
const autoLimitPercent = getPercent(autoHostingLimit);
|
||||
const viewLimitPercent = getPercent(viewLimit);
|
||||
const viewUsedPercentOfLimit = getPercent(viewBlobSpace, viewLimit);
|
||||
const autoUsedPercentOfLimit = getPercent(autoBlobSpace, autoHostingLimit);
|
||||
|
||||
return (
|
||||
<div className={'storage__wrapper'}>
|
||||
<div className={'storage__bar'}>
|
||||
<div className={'storage__other'} style={{ width: `${otherPercent}%` }} />
|
||||
<div className={'storage__private'} style={{ width: `${privatePercent}%` }} />
|
||||
<div className={'storage__auto'} style={{ width: `${autoLimitPercent}%` }}>
|
||||
<div className={'storage__auto--used'} style={{ width: `${autoUsedPercentOfLimit}%` }} />
|
||||
<div className={'storage__auto--free'} />
|
||||
</div>
|
||||
<div className={'storage__viewed'} style={{ width: `${viewLimitPercent}%` }}>
|
||||
<div className={'storage__viewed--used'} style={{ width: `${viewUsedPercentOfLimit}%` }} />
|
||||
<div className={'storage__viewed--free'} />
|
||||
</div>
|
||||
{viewHostingLimit !== 0 && <div style={{ 'background-color': 'unset' }} />}
|
||||
</div>
|
||||
<div className={'storage__legend-wrapper'}>
|
||||
<div className={'storage__legend-item'}>
|
||||
<div className={'storage__legend-item-swatch storage__legend-item-swatch--private'} />
|
||||
<div className={'storage__legend-item-label'}>
|
||||
<label>{__('Publishes --[legend, storage category]--')}</label>
|
||||
<div className={'help'}>{`${getGB(privateBlobSpace)} GB`}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'storage__legend-item'}>
|
||||
<div className={'storage__legend-item-swatch storage__legend-item-swatch--auto'} />
|
||||
<div className={'storage__legend-item-label'}>
|
||||
<label>{__('Auto Hosting --[legend, storage category]--')}</label>
|
||||
<div className={'help'}>
|
||||
{autoHostingLimit === 0 ? (
|
||||
__('Disabled')
|
||||
) : (
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
spaceUsed: getGB(autoBlobSpace),
|
||||
limit: getGB(autoHostingLimit),
|
||||
}}
|
||||
>
|
||||
%spaceUsed% of %limit% GB
|
||||
</I18nMessage>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'storage__legend-item'}>
|
||||
<div className={'storage__legend-item-swatch storage__legend-item-swatch--viewed'} />
|
||||
<div className={'storage__legend-item-label'}>
|
||||
<label>{__('View Hosting --[legend, storage category]--')}</label>
|
||||
<div className={'help'}>
|
||||
{viewHostingLimit === 1 ? (
|
||||
__('Disabled')
|
||||
) : (
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
spaceUsed: getGB(viewBlobSpace),
|
||||
limit: viewHostingLimit !== 0 ? getGB(viewHostingLimit) : getGB(viewFree),
|
||||
}}
|
||||
>
|
||||
%spaceUsed% of %limit% Free GB
|
||||
</I18nMessage>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{viewHostingLimit !== 0 && (
|
||||
<div className={'storage__legend-item'}>
|
||||
<div className={'storage__legend-item-swatch storage__legend-item-swatch--free'} />
|
||||
<div className={'storage__legend-item-label'}>
|
||||
<label>{__('Free --[legend, unused disk space]--')}</label>
|
||||
<div className={'help'}>{`${getGB(unallocFree)} GB`}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StorageViz;
|
27
ui/component/hostingSplash/index.js
Normal file
27
ui/component/hostingSplash/index.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import HostingSplash from './view';
|
||||
import {
|
||||
selectViewBlobSpace,
|
||||
selectViewHostingLimit,
|
||||
selectAutoBlobSpace,
|
||||
selectAutoHostingLimit,
|
||||
selectSaveBlobs,
|
||||
} from 'redux/selectors/settings';
|
||||
import { doSetDaemonSetting } from 'redux/actions/settings';
|
||||
import { selectDiskSpace } from 'redux/selectors/app';
|
||||
|
||||
const select = (state) => ({
|
||||
diskSpace: selectDiskSpace(state),
|
||||
viewHostingLimit: selectViewHostingLimit(state),
|
||||
autoHostingLimit: selectAutoHostingLimit(state),
|
||||
viewBlobSpace: selectViewBlobSpace(state),
|
||||
autoBlobSpace: selectAutoBlobSpace(state),
|
||||
saveBlobs: selectSaveBlobs(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(HostingSplash);
|
156
ui/component/hostingSplash/view.jsx
Normal file
156
ui/component/hostingSplash/view.jsx
Normal file
|
@ -0,0 +1,156 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { FormField } from 'component/common/form-components/form-field';
|
||||
import { Form } from 'component/common/form-components/form';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
// $FlowFixMe cannot resolve ...
|
||||
import image from 'static/img/yrblhappy.svg';
|
||||
import * as DAEMON_SETTINGS from 'constants/daemon_settings';
|
||||
|
||||
type SetDaemonSettingArg = boolean | string | number;
|
||||
|
||||
type Props = {
|
||||
handleNextPage: () => void,
|
||||
handleDone: () => void,
|
||||
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
|
||||
// --- select ---
|
||||
diskSpace: DiskSpace, // KB
|
||||
viewHostingLimit: number, // MB
|
||||
autoHostingLimit: number,
|
||||
viewBlobSpace: number,
|
||||
autoBlobSpace: number,
|
||||
privateBlobSpace: number,
|
||||
saveBlobs: boolean,
|
||||
};
|
||||
|
||||
const TWENTY_PERCENT = 0.2;
|
||||
const TEN_PRECNT = 0.1;
|
||||
|
||||
function HostingSplash(props: Props) {
|
||||
const {
|
||||
handleNextPage,
|
||||
diskSpace,
|
||||
viewHostingLimit,
|
||||
autoHostingLimit,
|
||||
viewBlobSpace,
|
||||
autoBlobSpace,
|
||||
saveBlobs,
|
||||
setDaemonSetting,
|
||||
handleDone,
|
||||
} = props;
|
||||
|
||||
const totalMB = diskSpace && Math.floor(Number(diskSpace.total) / 1024);
|
||||
const freeMB = diskSpace && Math.floor(Number(diskSpace.free) / 1024);
|
||||
const blobSpaceUsed = viewBlobSpace + autoBlobSpace;
|
||||
|
||||
const [hostingChoice, setHostingChoice] = React.useState('MANAGED');
|
||||
function handleSubmit() {
|
||||
if (hostingChoice === 'CUSTOM') {
|
||||
handleNextPage();
|
||||
} else {
|
||||
handleAuto();
|
||||
}
|
||||
}
|
||||
|
||||
function getManagedLimitMB() {
|
||||
const value =
|
||||
freeMB > totalMB * TWENTY_PERCENT // lots of free space?
|
||||
? blobSpaceUsed > totalMB * TEN_PRECNT // using more than 10%?
|
||||
? (freeMB + blobSpaceUsed) / 2 // e.g. 40g used plus 30g free, knock back to 35g limit, freeing to 35g
|
||||
: totalMB * TEN_PRECNT // let it go up to 10%
|
||||
: (freeMB + blobSpaceUsed) / 2; // e.g. 40g used plus 10g free, knock back to 25g limit, freeing to 25g
|
||||
return value > 10240 ? Math.floor(value / 1024) * 1024 : 0;
|
||||
}
|
||||
|
||||
function getAutoLimit() {
|
||||
// return floor of 10% of total
|
||||
const totalGB = Math.floor(getManagedLimitMB() / 1024); // eg, 25GB
|
||||
return Math.floor(totalGB / 10) * 1024; // eg, 2 GB -> 2048MB
|
||||
}
|
||||
|
||||
function getViewedLimit() {
|
||||
return getManagedLimitMB() - getAutoLimit();
|
||||
}
|
||||
|
||||
function getManagedCopy() {
|
||||
if (viewHostingLimit || autoHostingLimit || !saveBlobs) {
|
||||
return __("I'm happy with my settings");
|
||||
} else if (getManagedLimitMB() > 0) {
|
||||
return __(`Host up to %percent% of my drive (%limit% GB)`, {
|
||||
percent: `${Math.round((Math.floor(getManagedLimitMB() / 1024) / Math.floor(totalMB / 1024)) * 100)}%`,
|
||||
limit: Math.floor(getManagedLimitMB() / 1024),
|
||||
});
|
||||
} else {
|
||||
return __(`Not now, my disk is almost full.`);
|
||||
}
|
||||
}
|
||||
|
||||
function getManagedHelper() {
|
||||
if (viewHostingLimit || autoHostingLimit || !saveBlobs) {
|
||||
return __(`We've noticed you already have some settings.`);
|
||||
} else if (getManagedLimitMB() > 0) {
|
||||
return __(`Donate space without filling up your drive.`);
|
||||
} else {
|
||||
return __(`You can clear some space and check hosting settings later.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAuto() {
|
||||
if (viewHostingLimit || autoHostingLimit || !saveBlobs) {
|
||||
handleDone();
|
||||
} else if (getManagedLimitMB() > 0) {
|
||||
// limit to used // maybe move this to a single action function that doesn't live inside the component.
|
||||
await setDaemonSetting(DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB, getViewedLimit());
|
||||
await setDaemonSetting(DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB, getAutoLimit());
|
||||
handleDone();
|
||||
} else {
|
||||
// running low on space
|
||||
handleDone();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="main--contained">
|
||||
<div className={'columns first-run__wrapper'}>
|
||||
<div className={'first-run__left'}>
|
||||
<div>
|
||||
<h1 className="section__title--large">{__('Hosting')}</h1>
|
||||
<h3 className="section__subtitle">
|
||||
{__('Help creators and improve the P2P data network by hosting content.')}
|
||||
</h3>
|
||||
<fieldset>
|
||||
<FormField
|
||||
name={'managedhosting'}
|
||||
type="radio"
|
||||
checked={hostingChoice === 'MANAGED'}
|
||||
label={getManagedCopy()}
|
||||
helper={getManagedHelper()}
|
||||
onChange={(e) => setHostingChoice('MANAGED')}
|
||||
/>
|
||||
<FormField
|
||||
name={'customhosting'}
|
||||
type="radio"
|
||||
checked={hostingChoice === 'CUSTOM'}
|
||||
label={<>{__('Custom')}</>}
|
||||
helper={__(`You choose how much data to host.`)}
|
||||
onChange={(e) => setHostingChoice('CUSTOM')}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
<Form onSubmit={handleSubmit} className="section__body">
|
||||
<div className={'card__actions'}>
|
||||
<Button button="primary" label={hostingChoice === 'CUSTOM' ? __('Next') : __(`Let's go`)} type="submit" />
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
<div className={'first-run__image-wrapper'}>
|
||||
<img src={image} className="privacy-img" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(HostingSplash);
|
3
ui/component/hostingSplashCustom/index.js
Normal file
3
ui/component/hostingSplashCustom/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import HostingSplashCustom from './view';
|
||||
|
||||
export default HostingSplashCustom;
|
31
ui/component/hostingSplashCustom/view.jsx
Normal file
31
ui/component/hostingSplashCustom/view.jsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { Form } from 'component/common/form-components/form';
|
||||
import SettingStorage from 'component/settingStorage';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
type Props = {
|
||||
handleNextPage: () => void,
|
||||
handleGoBack: () => void,
|
||||
};
|
||||
|
||||
function HostingSplashCustom(props: Props) {
|
||||
const { handleNextPage, handleGoBack } = props;
|
||||
|
||||
return (
|
||||
<section className="main--contained">
|
||||
<div className={'first-run__wrapper'}>
|
||||
<SettingStorage isWelcome />
|
||||
<Form onSubmit={handleNextPage} className="section__body">
|
||||
<div className={'card__actions'}>
|
||||
<Button button="primary" label={__(`Let's go`)} type="submit" />
|
||||
<Button button="link" label={__(`Go back`)} onClick={handleGoBack} />
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(HostingSplashCustom);
|
|
@ -72,9 +72,9 @@ function PrivacyAgreement(props: Props) {
|
|||
{__('No')} <span>😢</span>
|
||||
</>
|
||||
}
|
||||
helper={__(`* Note that as
|
||||
peer-to-peer software, your IP address and potentially other system information can be sent to other
|
||||
users, though this information is not stored permanently.`)}
|
||||
helper={__(
|
||||
`* Note that as peer-to-peer software, your IP address and potentially other system information can be sent to other users, though this information is not stored permanently.`
|
||||
)}
|
||||
onChange={(e) => setShare(NONE)}
|
||||
/>
|
||||
{authenticated && (
|
||||
|
@ -92,7 +92,7 @@ function PrivacyAgreement(props: Props) {
|
|||
)}
|
||||
</fieldset>
|
||||
<div className={'card__actions'}>
|
||||
<Button button="primary" label={__(`Let's go`)} disabled={!share} type="submit" />
|
||||
<Button button="primary" label={__(`Next`)} disabled={!share} type="submit" />
|
||||
</div>
|
||||
{share === NONE && (
|
||||
<p className="help">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doSetDaemonSetting, doGetDaemonStatus, doCleanBlobs } from 'redux/actions/settings';
|
||||
import { selectDaemonStatus, selectDaemonSettings } from 'redux/selectors/settings';
|
||||
import { selectDaemonStatus, selectDaemonSettings, selectSettingDaemonSettings } from 'redux/selectors/settings';
|
||||
import SettingWalletServer from './view';
|
||||
import { selectDiskSpace } from 'redux/selectors/app';
|
||||
|
||||
|
@ -8,6 +8,7 @@ const select = (state) => ({
|
|||
daemonSettings: selectDaemonSettings(state),
|
||||
daemonStatus: selectDaemonStatus(state),
|
||||
diskSpace: selectDiskSpace(state),
|
||||
isSetting: selectSettingDaemonSettings(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
|
|
|
@ -4,89 +4,36 @@ import React from 'react';
|
|||
import { FormField } from 'component/common/form';
|
||||
import Button from 'component/button';
|
||||
import * as DAEMON_SETTINGS from 'constants/daemon_settings';
|
||||
import { formatBytes } from 'util/format-bytes';
|
||||
import { isTrulyANumber } from 'util/number';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
const BYTES_PER_MB = 1048576;
|
||||
const ENABLE_AUTOMATIC_HOSTING = false;
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as KEYCODES from 'constants/keycodes';
|
||||
|
||||
type Price = {
|
||||
currency: string,
|
||||
amount: number,
|
||||
};
|
||||
import { convertGbToMbStr, isValidHostingAmount } from 'util/hosting';
|
||||
|
||||
type DaemonStatus = {
|
||||
disk_space: {
|
||||
content_blobs_storage_used_mb: string,
|
||||
published_blobs_storage_used_mb: string,
|
||||
running: true,
|
||||
seed_blobs_storage_used_mb: string,
|
||||
total_used_mb: string,
|
||||
},
|
||||
};
|
||||
|
||||
type SetDaemonSettingArg = boolean | string | number | Price;
|
||||
type SetDaemonSettingArg = boolean | string | number;
|
||||
|
||||
type DaemonSettings = {
|
||||
download_dir: string,
|
||||
share_usage_data: boolean,
|
||||
max_key_fee?: Price,
|
||||
max_connections_per_download?: number,
|
||||
save_files: boolean,
|
||||
save_blobs: boolean,
|
||||
ffmpeg_path: string,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
// --- select ---
|
||||
daemonSettings: DaemonSettings,
|
||||
daemonStatus: DaemonStatus,
|
||||
// --- perform ---
|
||||
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
|
||||
cleanBlobs: () => string,
|
||||
diskSpace?: DiskSpace,
|
||||
getDaemonStatus: () => void,
|
||||
isSetting: boolean,
|
||||
};
|
||||
|
||||
function SettingDataHosting(props: Props) {
|
||||
const { daemonSettings, daemonStatus, setDaemonSetting, cleanBlobs, diskSpace, getDaemonStatus } = props;
|
||||
const { daemonSettings, setDaemonSetting, cleanBlobs, getDaemonStatus, isSetting } = props;
|
||||
|
||||
const { disk_space: blobSpace } = daemonStatus;
|
||||
const contentSpaceUsed = Number(blobSpace.content_blobs_storage_used_mb);
|
||||
const blobLimitSetting = daemonSettings[DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB] || '0';
|
||||
const [contentBlobSpaceLimitGB, setContentBlobSpaceLimit] = React.useState(
|
||||
blobLimitSetting ? String(blobLimitSetting / 1024) : '10'
|
||||
);
|
||||
const [applying, setApplying] = React.useState(false);
|
||||
|
||||
const networkSpaceUsed = Number(blobSpace.seed_blobs_storage_used_mb);
|
||||
const networkLimitSetting = daemonSettings[DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB] || '0';
|
||||
const networkLimitSetting = daemonSettings[DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB] || 0;
|
||||
const [networkBlobSpaceLimitGB, setNetworkBlobSpaceLimit] = React.useState(
|
||||
networkLimitSetting ? String(networkLimitSetting / 1024) : '0'
|
||||
networkLimitSetting ? String(networkLimitSetting / 1024) : 0
|
||||
);
|
||||
|
||||
const [unlimited, setUnlimited] = React.useState(blobLimitSetting === '0');
|
||||
|
||||
React.useEffect(() => {
|
||||
getDaemonStatus();
|
||||
}, []);
|
||||
|
||||
function convertGbToMb(gb) {
|
||||
return Number(gb) * 1024;
|
||||
}
|
||||
|
||||
function handleContentLimitChange(gb) {
|
||||
if (gb === '') {
|
||||
setContentBlobSpaceLimit('');
|
||||
} else if (gb === '0') {
|
||||
setContentBlobSpaceLimit('0.01'); // setting 0 means unlimited.
|
||||
} else {
|
||||
if (isTrulyANumber(Number(gb))) {
|
||||
setContentBlobSpaceLimit(gb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleNetworkLimitChange(gb) {
|
||||
if (gb === '') {
|
||||
setNetworkBlobSpaceLimit('');
|
||||
|
@ -98,109 +45,68 @@ function SettingDataHosting(props: Props) {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleApply() {
|
||||
setApplying(true);
|
||||
if (unlimited) {
|
||||
await setDaemonSetting(DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB, '0');
|
||||
} else {
|
||||
await setDaemonSetting(
|
||||
DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB,
|
||||
String(contentBlobSpaceLimitGB === '0.01' ? '1' : convertGbToMb(contentBlobSpaceLimitGB))
|
||||
);
|
||||
function handleKeyDown(e) {
|
||||
if (e.keyCode === KEYCODES.ESCAPE) {
|
||||
e.preventDefault();
|
||||
setNetworkBlobSpaceLimit(String(networkLimitSetting / 1024));
|
||||
} else if (e.keyCode === KEYCODES.ENTER) {
|
||||
e.preventDefault();
|
||||
handleApply();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApply() {
|
||||
await setDaemonSetting(
|
||||
DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB,
|
||||
String(convertGbToMb(Number(networkBlobSpaceLimitGB)))
|
||||
String(convertGbToMbStr(Number(networkBlobSpaceLimitGB)))
|
||||
);
|
||||
await cleanBlobs();
|
||||
getDaemonStatus();
|
||||
setApplying(false);
|
||||
}
|
||||
|
||||
function validHostingAmount(amountString) {
|
||||
const numberAmount = Number(amountString);
|
||||
return amountString.length && ((numberAmount && String(numberAmount)) || numberAmount === 0);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'fieldset-section'}>
|
||||
<FormField
|
||||
type="checkbox"
|
||||
name="save_blobs"
|
||||
onChange={() => setDaemonSetting('save_blobs', !daemonSettings.save_blobs)}
|
||||
checked={daemonSettings.save_blobs}
|
||||
label={__('Enable Data Hosting')}
|
||||
helper={
|
||||
diskSpace && (
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
free: formatBytes(Number(diskSpace.free) * 1024, 0),
|
||||
total: formatBytes(Number(diskSpace.total) * 1024, 0),
|
||||
}}
|
||||
>
|
||||
%free% of %total% available
|
||||
</I18nMessage>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{daemonSettings.save_blobs && (
|
||||
<div className={'fieldset-section'}>
|
||||
<FormField
|
||||
type="radio"
|
||||
name="no_hosting_limit"
|
||||
checked={unlimited}
|
||||
label={__('Unlimited View Hosting')}
|
||||
onChange={() => setUnlimited(true)}
|
||||
/>
|
||||
<FormField
|
||||
type="radio"
|
||||
name="set_hosting_limit"
|
||||
checked={!unlimited}
|
||||
onChange={() => setUnlimited(false)}
|
||||
label={__('Choose View Hosting Limit')}
|
||||
/>
|
||||
{!unlimited && (
|
||||
<FormField
|
||||
name="content_blob_limit_gb"
|
||||
type="number"
|
||||
min={0}
|
||||
onWheel={(e) => e.preventDefault()}
|
||||
label={__(`View Hosting Limit (GB)`)}
|
||||
onChange={(e) => handleContentLimitChange(e.target.value)}
|
||||
value={Number(contentBlobSpaceLimitGB) <= Number('0.01') ? '0' : contentBlobSpaceLimitGB}
|
||||
/>
|
||||
)}
|
||||
<div className={'help'}>{`Currently using ${formatBytes(contentSpaceUsed * BYTES_PER_MB)}`}</div>
|
||||
</div>
|
||||
)}
|
||||
{daemonSettings.save_blobs && ENABLE_AUTOMATIC_HOSTING && (
|
||||
<fieldset-section>
|
||||
<FormField
|
||||
name="network_blob_limit_gb"
|
||||
type="number"
|
||||
label={__(`Automatic Hosting (GB)`)}
|
||||
onChange={(e) => handleNetworkLimitChange(e.target.value)}
|
||||
value={networkBlobSpaceLimitGB}
|
||||
/>
|
||||
<div className={'help'}>{`Auto-hosting ${formatBytes(networkSpaceUsed * BYTES_PER_MB)}`}</div>
|
||||
</fieldset-section>
|
||||
)}
|
||||
<div className={'card__actions'}>
|
||||
disabled={!daemonSettings.save_blobs || isSetting}
|
||||
onKeyDown={handleKeyDown}
|
||||
inputButton={
|
||||
<>
|
||||
<Button
|
||||
disabled={
|
||||
(unlimited && blobLimitSetting === '0') ||
|
||||
(!unlimited &&
|
||||
(blobLimitSetting === convertGbToMb(contentBlobSpaceLimitGB) || // &&
|
||||
// networkLimitSetting === convertGbToMb(networkBlobSpaceLimitGB)
|
||||
!validHostingAmount(String(contentBlobSpaceLimitGB)))) ||
|
||||
applying
|
||||
// disabled if settings are equal or not valid amounts
|
||||
String(networkLimitSetting) === convertGbToMbStr(networkBlobSpaceLimitGB) ||
|
||||
!isValidHostingAmount(String(networkBlobSpaceLimitGB)) ||
|
||||
isSetting ||
|
||||
!daemonSettings.save_blobs
|
||||
}
|
||||
type="button"
|
||||
button="primary"
|
||||
button="alt"
|
||||
onClick={handleApply}
|
||||
label={__('Apply')}
|
||||
aria-label={__('Apply')}
|
||||
icon={ICONS.COMPLETE}
|
||||
/>
|
||||
<Button
|
||||
disabled={
|
||||
// disabled if settings are equal or not valid amounts
|
||||
String(networkLimitSetting) === convertGbToMbStr(networkBlobSpaceLimitGB) ||
|
||||
!isValidHostingAmount(String(networkBlobSpaceLimitGB)) ||
|
||||
isSetting ||
|
||||
!daemonSettings.save_blobs
|
||||
}
|
||||
type="button"
|
||||
button="alt"
|
||||
onClick={() => setNetworkBlobSpaceLimit(String(networkLimitSetting / 1024))}
|
||||
aria-label={__('Reset')}
|
||||
icon={ICONS.REMOVE}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
onChange={(e) => handleNetworkLimitChange(e.target.value)}
|
||||
value={networkBlobSpaceLimitGB}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
|
17
ui/component/settingSaveBlobs/index.js
Normal file
17
ui/component/settingSaveBlobs/index.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doSetDaemonSetting, doGetDaemonStatus } from 'redux/actions/settings';
|
||||
import { selectDaemonSettings } from 'redux/selectors/settings';
|
||||
import SettingWalletServer from './view';
|
||||
import { selectDiskSpace } from 'redux/selectors/app';
|
||||
|
||||
const select = (state) => ({
|
||||
daemonSettings: selectDaemonSettings(state),
|
||||
diskSpace: selectDiskSpace(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
getDaemonStatus: () => dispatch(doGetDaemonStatus()),
|
||||
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SettingWalletServer);
|
34
ui/component/settingSaveBlobs/view.jsx
Normal file
34
ui/component/settingSaveBlobs/view.jsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { FormField } from 'component/common/form';
|
||||
|
||||
type SetDaemonSettingArg = boolean | string | number;
|
||||
|
||||
type DaemonSettings = {
|
||||
save_blobs: boolean,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
// --- select ---
|
||||
daemonSettings: DaemonSettings,
|
||||
// --- perform ---
|
||||
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
|
||||
};
|
||||
|
||||
function SettingDataHosting(props: Props) {
|
||||
const { daemonSettings, setDaemonSetting } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
type="checkbox"
|
||||
name="save_blobs"
|
||||
onChange={() => setDaemonSetting('save_blobs', !daemonSettings.save_blobs)}
|
||||
checked={daemonSettings.save_blobs}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingDataHosting;
|
25
ui/component/settingStorage/index.js
Normal file
25
ui/component/settingStorage/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doWalletStatus } from 'redux/actions/wallet';
|
||||
import { doClearCache } from 'redux/actions/app';
|
||||
import { doSetDaemonSetting, doClearDaemonSetting, doCleanBlobs } from 'redux/actions/settings';
|
||||
import { selectDaemonSettings, selectDaemonStatus, selectSettingDaemonSettings } from 'redux/selectors/settings';
|
||||
|
||||
import SettingStorage from './view';
|
||||
import { selectDiskSpace } from 'redux/selectors/app';
|
||||
|
||||
const select = (state) => ({
|
||||
daemonSettings: selectDaemonSettings(state),
|
||||
diskSpace: selectDiskSpace(state),
|
||||
daemonStatus: selectDaemonStatus(state),
|
||||
isSetting: selectSettingDaemonSettings(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
|
||||
clearDaemonSetting: (key) => dispatch(doClearDaemonSetting(key)),
|
||||
clearCache: () => dispatch(doClearCache()),
|
||||
cleanBlobs: () => dispatch(doCleanBlobs()),
|
||||
updateWalletStatus: () => dispatch(doWalletStatus()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SettingStorage);
|
94
ui/component/settingStorage/view.jsx
Normal file
94
ui/component/settingStorage/view.jsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
// @flow
|
||||
import { SETTINGS_GRP } from 'constants/settings';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import SettingDataHosting from 'component/settingDataHosting';
|
||||
import SettingViewHosting from 'component/settingViewHosting';
|
||||
import SettingSaveBlobs from 'component/settingSaveBlobs';
|
||||
import SettingsRow from 'component/settingsRow';
|
||||
import AppStorageViz from 'component/appStorageVisualization';
|
||||
import Spinner from 'component/spinner';
|
||||
import classnames from 'classnames';
|
||||
|
||||
type DaemonSettings = {
|
||||
save_blobs: boolean,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
daemonSettings: DaemonSettings,
|
||||
isSetting: boolean,
|
||||
isWelcome?: boolean,
|
||||
cleanBlobs: () => Promise<any>,
|
||||
};
|
||||
|
||||
export default function SettingStorage(props: Props) {
|
||||
const { daemonSettings, isSetting, isWelcome, cleanBlobs } = props;
|
||||
|
||||
const saveBlobs = daemonSettings && daemonSettings.save_blobs;
|
||||
const [isCleaning, setCleaning] = React.useState(false);
|
||||
|
||||
// currently, it seems, blob space statistics are only updated during clean
|
||||
React.useEffect(() => {
|
||||
setCleaning(true);
|
||||
cleanBlobs().then(() => {
|
||||
setCleaning(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="card__title-section">
|
||||
<h2 className={classnames('card__title', { 'section__title--large': isWelcome })}>
|
||||
{isWelcome ? __('Custom Hosting') : __('Hosting')}
|
||||
{(isSetting || isCleaning) && <Spinner type={'small'} />}
|
||||
</h2>
|
||||
</div>
|
||||
<Card
|
||||
id={SETTINGS_GRP.SYSTEM}
|
||||
isBodyList
|
||||
body={
|
||||
<>
|
||||
<SettingsRow
|
||||
title={__('Enable Data Hosting')}
|
||||
subtitle={
|
||||
<React.Fragment>
|
||||
{__('Help improve the P2P data network (and make LBRY users happy) by hosting data.')}
|
||||
</React.Fragment>
|
||||
}
|
||||
footer={<AppStorageViz />}
|
||||
>
|
||||
<SettingSaveBlobs />
|
||||
</SettingsRow>
|
||||
<SettingsRow
|
||||
title={__('Viewed Hosting')}
|
||||
multirow
|
||||
disabled={!saveBlobs}
|
||||
subtitle={
|
||||
<React.Fragment>
|
||||
{__("View History Hosting lets you choose how much storage to use hosting content you've consumed.")}{' '}
|
||||
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/host-content" />
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
<SettingViewHosting disabled={!saveBlobs} />
|
||||
</SettingsRow>
|
||||
<SettingsRow
|
||||
title={__('Auto Hosting')}
|
||||
multirow
|
||||
disabled={!saveBlobs}
|
||||
subtitle={
|
||||
<React.Fragment>
|
||||
{__('Automatic Hosting downloads a small portion of content active on the network.')}{' '}
|
||||
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/host-content" />
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
<SettingDataHosting />
|
||||
</SettingsRow>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -10,7 +10,6 @@ import I18nMessage from 'component/i18nMessage';
|
|||
import SettingAutoLaunch from 'component/settingAutoLaunch';
|
||||
import SettingClosingBehavior from 'component/settingClosingBehavior';
|
||||
import SettingCommentsServer from 'component/settingCommentsServer';
|
||||
import SettingDataHosting from 'component/settingDataHosting';
|
||||
import SettingShareUrl from 'component/settingShareUrl';
|
||||
import SettingsRow from 'component/settingsRow';
|
||||
import SettingWalletServer from 'component/settingWalletServer';
|
||||
|
@ -151,24 +150,6 @@ export default function SettingSystem(props: Props) {
|
|||
checked={daemonSettings.save_files}
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow
|
||||
title={__('Data Hosting')}
|
||||
multirow
|
||||
subtitle={
|
||||
<React.Fragment>
|
||||
{__('Help improve the P2P data network (and make LBRY happy) by hosting data.')}{' '}
|
||||
{__("View History Hosting lets you choose how much storage to use helping content you've consumed.")}{' '}
|
||||
{/* {__( */}
|
||||
{/* 'Automatic Hosting lets you delegate some amount of storage for the network to automatically download and host.' */}
|
||||
{/* )}{' '} */}
|
||||
{__('Playing videos may exceed your history hosting limit until cleanup runs every 30 minutes.')}
|
||||
<br />
|
||||
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/host-content" />
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
<SettingDataHosting />
|
||||
</SettingsRow>
|
||||
<SettingsRow
|
||||
title={__('Share usage and diagnostic data')}
|
||||
subtitle={
|
||||
|
|
20
ui/component/settingViewHosting/index.js
Normal file
20
ui/component/settingViewHosting/index.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doSetDaemonSetting, doGetDaemonStatus, doCleanBlobs } from 'redux/actions/settings';
|
||||
import { selectViewHostingLimit, selectViewBlobSpace, selectSettingDaemonSettings } from 'redux/selectors/settings';
|
||||
import SettingViewHosting from './view';
|
||||
import { selectDiskSpace } from 'redux/selectors/app';
|
||||
|
||||
const select = (state) => ({
|
||||
viewHostingLimit: selectViewHostingLimit(state),
|
||||
viewBlobSpace: selectViewBlobSpace(state),
|
||||
diskSpace: selectDiskSpace(state),
|
||||
isSetting: selectSettingDaemonSettings(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
getDaemonStatus: () => dispatch(doGetDaemonStatus()),
|
||||
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
|
||||
cleanBlobs: () => dispatch(doCleanBlobs()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SettingViewHosting);
|
166
ui/component/settingViewHosting/view.jsx
Normal file
166
ui/component/settingViewHosting/view.jsx
Normal file
|
@ -0,0 +1,166 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { FormField } from 'component/common/form';
|
||||
import Button from 'component/button';
|
||||
import * as DAEMON_SETTINGS from 'constants/daemon_settings';
|
||||
import { isTrulyANumber } from 'util/number';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as KEYCODES from 'constants/keycodes';
|
||||
import { convertGbToMbStr, isValidHostingAmount } from 'util/hosting';
|
||||
|
||||
type SetDaemonSettingArg = boolean | string | number;
|
||||
|
||||
type Props = {
|
||||
// --- select ---
|
||||
viewBlobSpace: number,
|
||||
viewHostingLimit: number,
|
||||
disabled?: boolean,
|
||||
isSetting: boolean,
|
||||
// --- perform ---
|
||||
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
|
||||
cleanBlobs: () => string,
|
||||
getDaemonStatus: () => void,
|
||||
diskSpace: DiskSpace,
|
||||
};
|
||||
|
||||
const TWENTY_PERCENT = 0.2;
|
||||
const TEN_PERCENT = 0.1;
|
||||
const MINIMUM_VIEW_SETTING = '0.01';
|
||||
|
||||
function SettingViewHosting(props: Props) {
|
||||
const {
|
||||
diskSpace,
|
||||
viewHostingLimit,
|
||||
viewBlobSpace,
|
||||
setDaemonSetting,
|
||||
cleanBlobs,
|
||||
getDaemonStatus,
|
||||
disabled,
|
||||
isSetting,
|
||||
} = props;
|
||||
|
||||
// best effort to recommend a hosting amount default for the user
|
||||
const totalMB = diskSpace && Math.floor(Number(diskSpace.total) / 1024);
|
||||
const freeMB = diskSpace && Math.floor(Number(diskSpace.free) / 1024);
|
||||
const getGB = (val) => (Number(val) / 1024).toFixed(2);
|
||||
const recommendedSpace =
|
||||
freeMB > totalMB * TWENTY_PERCENT // plenty of space?
|
||||
? Math.ceil(Number(getGB(totalMB * TEN_PERCENT))) // 10% of total
|
||||
: Math.ceil(Number(getGB(viewBlobSpace))); // current amount to avoid deleting
|
||||
// daemon settings come in as 'number', but we manage them as 'String'.
|
||||
const [contentBlobSpaceLimitGB, setContentBlobSpaceLimit] = React.useState(
|
||||
viewHostingLimit === 0 ? String(recommendedSpace) : String(viewHostingLimit / 1024)
|
||||
);
|
||||
|
||||
const [unlimited, setUnlimited] = React.useState(viewHostingLimit === 0);
|
||||
|
||||
function handleContentLimitChange(gb) {
|
||||
if (gb === '') {
|
||||
setContentBlobSpaceLimit('');
|
||||
} else if (gb === '0') {
|
||||
setContentBlobSpaceLimit(MINIMUM_VIEW_SETTING); // setting 0 means unlimited.
|
||||
} else {
|
||||
if (isTrulyANumber(Number(gb))) {
|
||||
setContentBlobSpaceLimit(gb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApply() {
|
||||
if (unlimited) {
|
||||
await setDaemonSetting(DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB, '0');
|
||||
} else {
|
||||
await setDaemonSetting(
|
||||
DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB,
|
||||
contentBlobSpaceLimitGB === MINIMUM_VIEW_SETTING ? '1' : convertGbToMbStr(contentBlobSpaceLimitGB)
|
||||
);
|
||||
}
|
||||
await cleanBlobs();
|
||||
getDaemonStatus();
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
if (e.keyCode === KEYCODES.ESCAPE) {
|
||||
e.preventDefault();
|
||||
setContentBlobSpaceLimit(String(viewHostingLimit / 1024));
|
||||
} else if (e.keyCode === KEYCODES.ENTER) {
|
||||
e.preventDefault();
|
||||
handleApply();
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (unlimited) {
|
||||
handleApply();
|
||||
}
|
||||
}, [unlimited]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'fieldset-section'}>
|
||||
<FormField
|
||||
type="checkbox"
|
||||
name="hosting_limit"
|
||||
checked={unlimited}
|
||||
disabled={disabled || isSetting}
|
||||
label={__('Unlimited View Hosting')}
|
||||
onChange={() => setUnlimited(!unlimited)}
|
||||
/>
|
||||
<FormField
|
||||
name="content_blob_limit_gb"
|
||||
type="number"
|
||||
min={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
inputButton={
|
||||
<>
|
||||
<Button
|
||||
disabled={
|
||||
// disabled if settings are equal or not valid amounts
|
||||
(viewHostingLimit === 1 && contentBlobSpaceLimitGB <= MINIMUM_VIEW_SETTING) ||
|
||||
(unlimited && viewHostingLimit === 0) ||
|
||||
(!unlimited &&
|
||||
String(viewHostingLimit) ===
|
||||
convertGbToMbStr(
|
||||
contentBlobSpaceLimitGB || !isValidHostingAmount(String(contentBlobSpaceLimitGB))
|
||||
)) ||
|
||||
isSetting ||
|
||||
disabled
|
||||
}
|
||||
type="button"
|
||||
button="alt"
|
||||
onClick={handleApply}
|
||||
aria-label={__('Apply')}
|
||||
icon={ICONS.COMPLETE}
|
||||
/>
|
||||
<Button
|
||||
disabled={
|
||||
// disabled if settings are equal or not valid amounts
|
||||
(viewHostingLimit === 1 && contentBlobSpaceLimitGB <= MINIMUM_VIEW_SETTING) ||
|
||||
(unlimited && viewHostingLimit === 0) ||
|
||||
(!unlimited &&
|
||||
(String(viewHostingLimit) === convertGbToMbStr(contentBlobSpaceLimitGB) ||
|
||||
!isValidHostingAmount(String(contentBlobSpaceLimitGB)))) ||
|
||||
isSetting ||
|
||||
disabled
|
||||
}
|
||||
type="button"
|
||||
button="alt"
|
||||
onClick={() => setContentBlobSpaceLimit(String(viewHostingLimit / 1024))}
|
||||
aria-label={__('Reset')}
|
||||
icon={ICONS.REMOVE}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
disabled={isSetting || disabled || unlimited}
|
||||
onWheel={(e) => e.preventDefault()}
|
||||
label={__(`View Hosting Limit (GB)`)}
|
||||
onChange={(e) => handleContentLimitChange(e.target.value)}
|
||||
value={Number(contentBlobSpaceLimitGB) <= Number(MINIMUM_VIEW_SETTING) ? '0' : contentBlobSpaceLimitGB}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingViewHosting;
|
|
@ -9,10 +9,11 @@ type Props = {
|
|||
useVerticalSeparator?: boolean, // Show a separator line between Label and Value. Useful when there are multiple Values.
|
||||
disabled?: boolean,
|
||||
children?: React$Node,
|
||||
footer?: React$Node,
|
||||
};
|
||||
|
||||
export default function SettingsRow(props: Props) {
|
||||
const { title, subtitle, multirow, useVerticalSeparator, disabled, children } = props;
|
||||
const { title, subtitle, multirow, useVerticalSeparator, disabled, children, footer } = props;
|
||||
return (
|
||||
<div
|
||||
className={classnames('card__main-actions settings__row', {
|
||||
|
@ -23,6 +24,7 @@ export default function SettingsRow(props: Props) {
|
|||
<div className="settings__row--title">
|
||||
<p>{title}</p>
|
||||
{subtitle && <p className="settings__row--subtitle">{subtitle}</p>}
|
||||
{footer && footer}
|
||||
</div>
|
||||
<div
|
||||
className={classnames('settings__row--value', {
|
||||
|
|
|
@ -43,6 +43,11 @@ const SIDE_LINKS: Array<SideNavLink> = [
|
|||
section: SETTINGS_GRP.SYSTEM,
|
||||
icon: ICONS.SETTINGS,
|
||||
},
|
||||
{
|
||||
title: 'Content Hosting',
|
||||
section: SETTINGS_GRP.STORAGE,
|
||||
icon: ICONS.PUBLISH,
|
||||
},
|
||||
];
|
||||
|
||||
export default function SettingsSideNavigation() {
|
||||
|
|
|
@ -249,6 +249,7 @@ export const SYNC_CLIENT_SETTINGS = 'SYNC_CLIENT_SETTINGS';
|
|||
export const DAEMON_STATUS_RECEIVED = 'DAEMON_STATUS_RECEIVED';
|
||||
export const SHARED_PREFERENCE_SET = 'SHARED_PREFERENCE_SET';
|
||||
export const SAVE_CUSTOM_WALLET_SERVERS = 'SAVE_CUSTOM_WALLET_SERVERS';
|
||||
export const SETTING_DAEMON_SETTINGS = 'SETTING_DAEMON_SETTINGS';
|
||||
|
||||
// User
|
||||
export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED';
|
||||
|
|
|
@ -53,4 +53,5 @@ export const SETTINGS_GRP = {
|
|||
ACCOUNT: 'account',
|
||||
CONTENT: 'content',
|
||||
SYSTEM: 'system',
|
||||
STORAGE: 'Storage',
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ import SettingAccount from 'component/settingAccount';
|
|||
import SettingAppearance from 'component/settingAppearance';
|
||||
import SettingContent from 'component/settingContent';
|
||||
import SettingSystem from 'component/settingSystem';
|
||||
import SettingStorage from 'component/settingStorage';
|
||||
|
||||
type DaemonSettings = {
|
||||
download_dir: string,
|
||||
|
@ -51,6 +52,7 @@ class SettingsPage extends React.PureComponent<Props> {
|
|||
<SettingAccount />
|
||||
<SettingContent />
|
||||
<SettingSystem />
|
||||
<SettingStorage />
|
||||
</div>
|
||||
)}
|
||||
</Page>
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import PrivacyAgreement from 'component/privacyAgreement';
|
||||
import HostingSplash from 'component/hostingSplash';
|
||||
import HostingSplashCustom from 'component/hostingSplashCustom';
|
||||
import WelcomeSplash from 'component/welcomeSplash';
|
||||
import Page from 'component/page';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
const SPLASH_PAGE = 0;
|
||||
const PRIVACY_PAGE = 1;
|
||||
// const HOSTING_PAGE = 2;
|
||||
// const WELCOME_PAGES = [SPLASH_PAGE, PRIVACY_PAGE];
|
||||
const HOSTING_PAGE = 2;
|
||||
const HOSTING_ADVANCED = 3;
|
||||
|
||||
type DaemonStatus = {
|
||||
disk_space: {
|
||||
content_blobs_storage_used_mb: string,
|
||||
|
@ -44,6 +47,16 @@ export default function Welcome(props: Props) {
|
|||
const handleNextPage = () => {
|
||||
if (welcomePage === SPLASH_PAGE) {
|
||||
setWelcomePage(PRIVACY_PAGE);
|
||||
} else if (welcomePage === PRIVACY_PAGE) {
|
||||
setWelcomePage(HOSTING_PAGE);
|
||||
} else if (welcomePage === HOSTING_PAGE) {
|
||||
setWelcomePage(HOSTING_ADVANCED);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
if (welcomePage >= 1) {
|
||||
setWelcomePage(welcomePage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -55,8 +68,11 @@ export default function Welcome(props: Props) {
|
|||
return (
|
||||
<Page noHeader noSideNavigation>
|
||||
{welcomePage === SPLASH_PAGE && <WelcomeSplash handleNextPage={handleNextPage} />}
|
||||
{welcomePage === PRIVACY_PAGE && <PrivacyAgreement handleNextPage={handleDone} />}
|
||||
{/* {welcomePage === HOSTING_PAGE && } */}
|
||||
{welcomePage === PRIVACY_PAGE && <PrivacyAgreement handleNextPage={handleNextPage} />}
|
||||
{welcomePage === HOSTING_PAGE && <HostingSplash handleNextPage={handleNextPage} handleDone={handleDone} />}
|
||||
{welcomePage === HOSTING_ADVANCED && (
|
||||
<HostingSplashCustom handleNextPage={handleDone} handleGoBack={handleGoBack} />
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1197,7 +1197,7 @@ export function doFetchModBlockedList() {
|
|||
if (blockedChannel.blocked_channel_name) {
|
||||
const channelUri = buildURI({
|
||||
channelName: blockedChannel.blocked_channel_name,
|
||||
claimId: blockedChannel.blocked_channel_id,
|
||||
channelClaimId: blockedChannel.blocked_channel_id,
|
||||
});
|
||||
|
||||
if (!blockedList.find((blockedChannel) => isURIEqual(blockedChannel.channelUri, channelUri))) {
|
||||
|
@ -1215,7 +1215,7 @@ export function doFetchModBlockedList() {
|
|||
if (blockedByMap !== undefined) {
|
||||
const blockedByChannelUri = buildURI({
|
||||
channelName: blockedChannel.blocked_by_channel_name,
|
||||
claimId: blockedChannel.blocked_by_channel_id,
|
||||
channelClaimId: blockedChannel.blocked_by_channel_id,
|
||||
});
|
||||
|
||||
if (blockedByMap[channelUri]) {
|
||||
|
|
|
@ -104,16 +104,21 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
|
|||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const ready = selectPrefsReady(state);
|
||||
|
||||
if (!ready) {
|
||||
return dispatch(doAlertWaitingForSync());
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.SETTING_DAEMON_SETTINGS,
|
||||
data: {
|
||||
val: true,
|
||||
},
|
||||
});
|
||||
const newSettings = {
|
||||
key,
|
||||
value: !value && value !== false ? null : value,
|
||||
};
|
||||
Lbry.settings_set(newSettings).then((newSetting) => {
|
||||
Lbry.settings_set(newSettings)
|
||||
.then((newSetting) => {
|
||||
if (SDK_SYNC_KEYS.includes(key) && !doNotDispatch) {
|
||||
dispatch({
|
||||
type: ACTIONS.SHARED_PREFERENCE_SET,
|
||||
|
@ -125,14 +130,33 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
|
|||
dispatch(doWalletReconnect());
|
||||
// todo: add sdk reloadsettings() (or it happens automagically?)
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(doFetchDaemonSettings());
|
||||
})
|
||||
.then(() => {
|
||||
dispatch({
|
||||
type: ACTIONS.SETTING_DAEMON_SETTINGS,
|
||||
data: {
|
||||
val: false,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log('error setting or fetching daemon setting', e.message);
|
||||
dispatch({
|
||||
type: ACTIONS.SETTING_DAEMON_SETTINGS,
|
||||
data: {
|
||||
val: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doCleanBlobs() {
|
||||
return (dispatch) => {
|
||||
Lbry.blob_clean().then(() => {
|
||||
return Lbry.blob_clean().then(() => {
|
||||
dispatch(doFetchDaemonSettings());
|
||||
return 'done';
|
||||
});
|
||||
|
|
|
@ -327,10 +327,9 @@ reducers[ACTIONS.DISK_SPACE] = (state, action) => {
|
|||
};
|
||||
|
||||
reducers[ACTIONS.SYNC_STATE_POPULATE] = (state, action) => {
|
||||
const { welcomeVersion, allowAnalytics } = action.data;
|
||||
const { allowAnalytics } = action.data;
|
||||
return {
|
||||
...state,
|
||||
...(welcomeVersion !== undefined ? { welcomeVersion } : {}),
|
||||
...(allowAnalytics !== undefined ? { allowAnalytics } : {}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ settingLanguage.push('en');
|
|||
|
||||
const defaultState = {
|
||||
isNight: false,
|
||||
isSettingDaemonSettings: false,
|
||||
findingFFmpeg: false,
|
||||
loadedLanguages: [...Object.keys(window.i18n_messages), 'en'] || ['en'],
|
||||
customWalletServers: [],
|
||||
|
@ -93,6 +94,11 @@ reducers[ACTIONS.FINDING_FFMPEG_STARTED] = (state) =>
|
|||
findingFFmpeg: true,
|
||||
});
|
||||
|
||||
reducers[ACTIONS.SETTING_DAEMON_SETTINGS] = (state, action) =>
|
||||
Object.assign({}, state, {
|
||||
isSettingDaemonSettings: action.data.val,
|
||||
});
|
||||
|
||||
reducers[ACTIONS.FINDING_FFMPEG_COMPLETED] = (state) =>
|
||||
Object.assign({}, state, {
|
||||
findingFFmpeg: false,
|
||||
|
|
|
@ -9,10 +9,23 @@ const homepages = require('homepages');
|
|||
const selectState = (state) => state.settings || {};
|
||||
|
||||
export const selectDaemonSettings = createSelector(selectState, (state) => state.daemonSettings);
|
||||
export const selectSettingDaemonSettings = createSelector(selectState, (state) => state.isSettingDaemonSettings);
|
||||
|
||||
export const selectDaemonStatus = createSelector(selectState, (state) => state.daemonStatus);
|
||||
|
||||
export const selectFfmpegStatus = createSelector(selectDaemonStatus, (status) => status.ffmpeg_status);
|
||||
export const selectViewBlobSpace = createSelector(
|
||||
selectDaemonStatus,
|
||||
(status) => status.disk_space.content_blobs_storage_used_mb
|
||||
);
|
||||
export const selectAutoBlobSpace = createSelector(
|
||||
selectDaemonStatus,
|
||||
(status) => status.disk_space.seed_blobs_storage_used_mb
|
||||
);
|
||||
export const selectPrivateBlobSpace = createSelector(
|
||||
selectDaemonStatus,
|
||||
(status) => status.disk_space.published_blobs_storage_used_mb
|
||||
);
|
||||
|
||||
export const selectFindingFFmpeg = createSelector(selectState, (state) => state.findingFFmpeg || false);
|
||||
|
||||
|
@ -82,6 +95,10 @@ export const selectHomepageData = createSelector(
|
|||
}
|
||||
);
|
||||
|
||||
export const selectSaveBlobs = createSelector(selectDaemonSettings, (state) => state.save_blobs || false);
|
||||
export const selectAutoHostingLimit = createSelector(selectDaemonSettings, (state) => state.network_storage_limit || 0);
|
||||
export const selectViewHostingLimit = createSelector(selectDaemonSettings, (state) => state.blob_storage_limit || 0);
|
||||
|
||||
export const selectosNotificationsEnabled = makeSelectClientSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED);
|
||||
|
||||
export const selectDisableAutoUpdates = makeSelectClientSetting(SETTINGS.DISABLE_AUTO_UPDATES);
|
||||
|
|
|
@ -172,17 +172,28 @@ input-submit {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
& > *:not(:first-child):not(:last-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
& > *:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
& > *:last-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-right-radius: var(--border-radius);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
border-right: none;
|
||||
}
|
||||
// FIX THIS e.g. copyable text vs editable text
|
||||
//& > *:nth-child(2) {
|
||||
// border-top-left-radius: 0;
|
||||
// border-bottom-left-radius: 0;
|
||||
// border: 1px solid var(--color-input-border);
|
||||
//}
|
||||
}
|
||||
|
||||
.checkbox,
|
||||
|
|
|
@ -548,6 +548,7 @@ body {
|
|||
border-top: unset;
|
||||
|
||||
.settings__row {
|
||||
align-items: flex-start;
|
||||
padding: var(--spacing-s);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
.checkbox {
|
||||
|
|
|
@ -29,3 +29,95 @@
|
|||
text-align: right;
|
||||
padding-top: var(--spacing-m);
|
||||
}
|
||||
|
||||
.storage__wrapper {
|
||||
.storage__bar {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--spacing-xl);
|
||||
background-color: var(--color-storage-free);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
border-radius: var(--border-radius);
|
||||
> :last-of-type {
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
}
|
||||
.storage__other {
|
||||
height: 100%;
|
||||
background-color: var(--color-gray-7);
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
}
|
||||
.storage__private {
|
||||
height: 100%;
|
||||
background-color: var(--color-storage-published);
|
||||
}
|
||||
box-sizing: border-box;
|
||||
|
||||
.storage__viewed {
|
||||
height: 100%;
|
||||
background-color: var(--color-storage-viewed-free);
|
||||
border: var(--color-storage-viewed) 1px solid;
|
||||
box-sizing: border-box;
|
||||
.storage__viewed--used {
|
||||
height: 100%;
|
||||
background-color: var(--color-storage-viewed);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.storage__auto {
|
||||
height: 100%;
|
||||
background-color: var(--color-storage-auto-free);
|
||||
border: var(--color-storage-auto) 1px solid;
|
||||
box-sizing: border-box;
|
||||
.storage__auto--used {
|
||||
height: 100%;
|
||||
background-color: var(--color-storage-auto);
|
||||
}
|
||||
}
|
||||
}
|
||||
.storage__legend-wrapper {
|
||||
margin-top: var(--spacing-m);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
.storage__legend-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.storage__legend-item-swatch {
|
||||
padding: var(--spacing-xs);
|
||||
margin-right: var(--spacing-s);
|
||||
width: var(--spacing-l);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
.storage__legend-item-label {
|
||||
margin-right: var(--spacing-m);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.help {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.storage__legend-item-swatch--other {
|
||||
background-color: var(--color-gray-7);
|
||||
}
|
||||
.storage__legend-item-swatch--private {
|
||||
background-color: var(--color-storage-published);
|
||||
}
|
||||
.storage__legend-item-swatch--viewed {
|
||||
background-color: var(--color-storage-viewed);
|
||||
}
|
||||
.storage__legend-item-swatch--auto {
|
||||
background-color: var(--color-storage-auto);
|
||||
}
|
||||
.storage__legend-item-swatch--free {
|
||||
background-color: var(--color-storage-free);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -293,6 +293,9 @@
|
|||
&:only-child {
|
||||
border-top: none;
|
||||
}
|
||||
&.section__actions--between {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.card__main-actions.settings__row {
|
||||
|
@ -301,6 +304,7 @@
|
|||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
padding-bottom: var(--spacing-m);
|
||||
}
|
||||
|
||||
.settings__row--title {
|
||||
|
|
|
@ -901,23 +901,6 @@ img {
|
|||
}
|
||||
}
|
||||
|
||||
.scheduledLivestream-wrapper {
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: var(--spacing-s);
|
||||
padding-top: 0;
|
||||
|
||||
.card__main-actions {
|
||||
.claim-preview__wrapper {
|
||||
a {
|
||||
.button__content {
|
||||
align-items: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary master classes
|
||||
.date_time {
|
||||
font-size: var(--font-xsmall);
|
||||
|
|
|
@ -200,4 +200,13 @@
|
|||
radial-gradient(circle at 50% 117%, rgba(25, 25, 25, 0.2) 0, #202020 100%);
|
||||
|
||||
--mui-background: #000;
|
||||
|
||||
// storage vis
|
||||
--color-storage-published: var(--color-brand-blue);
|
||||
--color-storage-free: var(--color-gray-4);
|
||||
--color-storage-used: var(--color-gray-7);
|
||||
--color-storage-auto: #ff993c;
|
||||
--color-storage-auto-free: #9f5f25;
|
||||
--color-storage-viewed: #a93cff;
|
||||
--color-storage-viewed-free: #602192;
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
// Input
|
||||
--color-input-bg-selected: var(--color-primary-alt);
|
||||
--color-input-color: #111111;
|
||||
--color-input-label: var(--color-gray-5);
|
||||
--color-input-label: var(--color-text-base);
|
||||
--color-input-placeholder: #212529;
|
||||
--color-input-bg: var(--color-white);
|
||||
--color-input-border: var(--color-border);
|
||||
|
@ -206,4 +206,13 @@
|
|||
|
||||
--mui-background: #fff;
|
||||
--mui-button: var(--color-header-button);
|
||||
|
||||
// Storage vis
|
||||
--color-storage-published: var(--color-brand-blue);
|
||||
--color-storage-free: var(--color-gray-4);
|
||||
--color-storage-used: var(--color-gray-7);
|
||||
--color-storage-auto: #ff993c;
|
||||
--color-storage-auto-free: #9f5f25;
|
||||
--color-storage-viewed: #a93cff;
|
||||
--color-storage-viewed-free: #602192;
|
||||
}
|
||||
|
|
8
ui/util/hosting.js
Normal file
8
ui/util/hosting.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export function convertGbToMbStr(gb) {
|
||||
return String(Number(gb) * 1024);
|
||||
}
|
||||
|
||||
export function isValidHostingAmount(amountString) {
|
||||
const numberAmount = Number(amountString);
|
||||
return amountString.length && ((numberAmount && String(numberAmount)) || numberAmount === 0);
|
||||
}
|
Loading…
Reference in a new issue