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
|
SEARCH_SERVER_API=https://lighthouse.odysee.com/search
|
||||||
SOCKETY_SERVER_API=wss://sockety.odysee.com/ws
|
SOCKETY_SERVER_API=wss://sockety.odysee.com/ws
|
||||||
THUMBNAIL_CDN_URL=https://image-processor.vanwanet.com/optimize/
|
THUMBNAIL_CDN_URL=https://image-processor.vanwanet.com/optimize/
|
||||||
WELCOME_VERSION=1.1
|
WELCOME_VERSION=1.2
|
||||||
|
|
||||||
# STRIPE
|
# STRIPE
|
||||||
# STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo'
|
# 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.",
|
"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.",
|
"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",
|
"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",
|
"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.",
|
"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%",
|
"Choose %asset%": "Choose %asset%",
|
||||||
|
@ -2228,13 +2221,6 @@
|
||||||
"Enable Prerelease Updates": "Enable Prerelease Updates",
|
"Enable Prerelease Updates": "Enable Prerelease Updates",
|
||||||
"Enable Upgrade to Test Builds": "Enable Upgrade to Test Builds",
|
"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.",
|
"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",
|
"A channel is required to repost on LBRY": "A channel is required to repost on LBRY",
|
||||||
"Admin": "Admin",
|
"Admin": "Admin",
|
||||||
"Stickers": "Stickers",
|
"Stickers": "Stickers",
|
||||||
|
@ -2256,19 +2242,6 @@
|
||||||
"Move Up": "Move Up",
|
"Move Up": "Move Up",
|
||||||
"Move Down": "Move Down",
|
"Move Down": "Move Down",
|
||||||
"Trending for #Game": "Trending for #Game",
|
"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",
|
"Remove custom comment server": "Remove custom comment server",
|
||||||
"Use Https": "Use Https",
|
"Use Https": "Use Https",
|
||||||
"Server URL": "Server URL",
|
"Server URL": "Server URL",
|
||||||
|
@ -2315,8 +2288,27 @@
|
||||||
"Clear Views": "Clear Views",
|
"Clear Views": "Clear Views",
|
||||||
"Show Video View Progress": "Show Video View Progress",
|
"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.",
|
"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%",
|
"Content Hosting": "Content Hosting",
|
||||||
"Anon --[used in <%anonymous% Reposted>]--": "Anon",
|
"Hosting": "Hosting",
|
||||||
"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.",
|
"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--"
|
"--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>
|
{__('No')} <span>😢</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
helper={__(`* Note that as
|
helper={__(
|
||||||
peer-to-peer software, your IP address and potentially other system information can be sent to other
|
`* 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.`
|
||||||
users, though this information is not stored permanently.`)}
|
)}
|
||||||
onChange={(e) => setShare(NONE)}
|
onChange={(e) => setShare(NONE)}
|
||||||
/>
|
/>
|
||||||
{authenticated && (
|
{authenticated && (
|
||||||
|
@ -92,7 +92,7 @@ function PrivacyAgreement(props: Props) {
|
||||||
)}
|
)}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div className={'card__actions'}>
|
<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>
|
</div>
|
||||||
{share === NONE && (
|
{share === NONE && (
|
||||||
<p className="help">
|
<p className="help">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doSetDaemonSetting, doGetDaemonStatus, doCleanBlobs } from 'redux/actions/settings';
|
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 SettingWalletServer from './view';
|
||||||
import { selectDiskSpace } from 'redux/selectors/app';
|
import { selectDiskSpace } from 'redux/selectors/app';
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ const select = (state) => ({
|
||||||
daemonSettings: selectDaemonSettings(state),
|
daemonSettings: selectDaemonSettings(state),
|
||||||
daemonStatus: selectDaemonStatus(state),
|
daemonStatus: selectDaemonStatus(state),
|
||||||
diskSpace: selectDiskSpace(state),
|
diskSpace: selectDiskSpace(state),
|
||||||
|
isSetting: selectSettingDaemonSettings(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
|
|
|
@ -4,89 +4,36 @@ import React from 'react';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import * as DAEMON_SETTINGS from 'constants/daemon_settings';
|
import * as DAEMON_SETTINGS from 'constants/daemon_settings';
|
||||||
import { formatBytes } from 'util/format-bytes';
|
|
||||||
import { isTrulyANumber } from 'util/number';
|
import { isTrulyANumber } from 'util/number';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import * as ICONS from 'constants/icons';
|
||||||
const BYTES_PER_MB = 1048576;
|
import * as KEYCODES from 'constants/keycodes';
|
||||||
const ENABLE_AUTOMATIC_HOSTING = false;
|
|
||||||
|
|
||||||
type Price = {
|
import { convertGbToMbStr, isValidHostingAmount } from 'util/hosting';
|
||||||
currency: string,
|
|
||||||
amount: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
type DaemonStatus = {
|
type SetDaemonSettingArg = boolean | string | number;
|
||||||
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 DaemonSettings = {
|
type DaemonSettings = {
|
||||||
download_dir: string,
|
|
||||||
share_usage_data: boolean,
|
|
||||||
max_key_fee?: Price,
|
|
||||||
max_connections_per_download?: number,
|
|
||||||
save_files: boolean,
|
|
||||||
save_blobs: boolean,
|
save_blobs: boolean,
|
||||||
ffmpeg_path: string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
// --- select ---
|
// --- select ---
|
||||||
daemonSettings: DaemonSettings,
|
daemonSettings: DaemonSettings,
|
||||||
daemonStatus: DaemonStatus,
|
|
||||||
// --- perform ---
|
// --- perform ---
|
||||||
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
|
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
|
||||||
cleanBlobs: () => string,
|
cleanBlobs: () => string,
|
||||||
diskSpace?: DiskSpace,
|
|
||||||
getDaemonStatus: () => void,
|
getDaemonStatus: () => void,
|
||||||
|
isSetting: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function SettingDataHosting(props: Props) {
|
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 networkLimitSetting = daemonSettings[DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB] || 0;
|
||||||
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 [networkBlobSpaceLimitGB, setNetworkBlobSpaceLimit] = React.useState(
|
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) {
|
function handleNetworkLimitChange(gb) {
|
||||||
if (gb === '') {
|
if (gb === '') {
|
||||||
setNetworkBlobSpaceLimit('');
|
setNetworkBlobSpaceLimit('');
|
||||||
|
@ -98,109 +45,68 @@ function SettingDataHosting(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleApply() {
|
function handleKeyDown(e) {
|
||||||
setApplying(true);
|
if (e.keyCode === KEYCODES.ESCAPE) {
|
||||||
if (unlimited) {
|
e.preventDefault();
|
||||||
await setDaemonSetting(DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB, '0');
|
setNetworkBlobSpaceLimit(String(networkLimitSetting / 1024));
|
||||||
} else {
|
} else if (e.keyCode === KEYCODES.ENTER) {
|
||||||
await setDaemonSetting(
|
e.preventDefault();
|
||||||
DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB,
|
handleApply();
|
||||||
String(contentBlobSpaceLimitGB === '0.01' ? '1' : convertGbToMb(contentBlobSpaceLimitGB))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleApply() {
|
||||||
await setDaemonSetting(
|
await setDaemonSetting(
|
||||||
DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB,
|
DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB,
|
||||||
String(convertGbToMb(Number(networkBlobSpaceLimitGB)))
|
String(convertGbToMbStr(Number(networkBlobSpaceLimitGB)))
|
||||||
);
|
);
|
||||||
await cleanBlobs();
|
await cleanBlobs();
|
||||||
getDaemonStatus();
|
getDaemonStatus();
|
||||||
setApplying(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validHostingAmount(amountString) {
|
|
||||||
const numberAmount = Number(amountString);
|
|
||||||
return amountString.length && ((numberAmount && String(numberAmount)) || numberAmount === 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={'fieldset-section'}>
|
<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
|
<FormField
|
||||||
name="network_blob_limit_gb"
|
name="network_blob_limit_gb"
|
||||||
type="number"
|
type="number"
|
||||||
label={__(`Automatic Hosting (GB)`)}
|
label={__(`Automatic Hosting (GB)`)}
|
||||||
onChange={(e) => handleNetworkLimitChange(e.target.value)}
|
disabled={!daemonSettings.save_blobs || isSetting}
|
||||||
value={networkBlobSpaceLimitGB}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
inputButton={
|
||||||
<div className={'help'}>{`Auto-hosting ${formatBytes(networkSpaceUsed * BYTES_PER_MB)}`}</div>
|
<>
|
||||||
</fieldset-section>
|
|
||||||
)}
|
|
||||||
<div className={'card__actions'}>
|
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
(unlimited && blobLimitSetting === '0') ||
|
// disabled if settings are equal or not valid amounts
|
||||||
(!unlimited &&
|
String(networkLimitSetting) === convertGbToMbStr(networkBlobSpaceLimitGB) ||
|
||||||
(blobLimitSetting === convertGbToMb(contentBlobSpaceLimitGB) || // &&
|
!isValidHostingAmount(String(networkBlobSpaceLimitGB)) ||
|
||||||
// networkLimitSetting === convertGbToMb(networkBlobSpaceLimitGB)
|
isSetting ||
|
||||||
!validHostingAmount(String(contentBlobSpaceLimitGB)))) ||
|
!daemonSettings.save_blobs
|
||||||
applying
|
|
||||||
}
|
}
|
||||||
type="button"
|
type="button"
|
||||||
button="primary"
|
button="alt"
|
||||||
onClick={handleApply}
|
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>
|
</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 SettingAutoLaunch from 'component/settingAutoLaunch';
|
||||||
import SettingClosingBehavior from 'component/settingClosingBehavior';
|
import SettingClosingBehavior from 'component/settingClosingBehavior';
|
||||||
import SettingCommentsServer from 'component/settingCommentsServer';
|
import SettingCommentsServer from 'component/settingCommentsServer';
|
||||||
import SettingDataHosting from 'component/settingDataHosting';
|
|
||||||
import SettingShareUrl from 'component/settingShareUrl';
|
import SettingShareUrl from 'component/settingShareUrl';
|
||||||
import SettingsRow from 'component/settingsRow';
|
import SettingsRow from 'component/settingsRow';
|
||||||
import SettingWalletServer from 'component/settingWalletServer';
|
import SettingWalletServer from 'component/settingWalletServer';
|
||||||
|
@ -151,24 +150,6 @@ export default function SettingSystem(props: Props) {
|
||||||
checked={daemonSettings.save_files}
|
checked={daemonSettings.save_files}
|
||||||
/>
|
/>
|
||||||
</SettingsRow>
|
</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
|
<SettingsRow
|
||||||
title={__('Share usage and diagnostic data')}
|
title={__('Share usage and diagnostic data')}
|
||||||
subtitle={
|
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.
|
useVerticalSeparator?: boolean, // Show a separator line between Label and Value. Useful when there are multiple Values.
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
children?: React$Node,
|
children?: React$Node,
|
||||||
|
footer?: React$Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SettingsRow(props: Props) {
|
export default function SettingsRow(props: Props) {
|
||||||
const { title, subtitle, multirow, useVerticalSeparator, disabled, children } = props;
|
const { title, subtitle, multirow, useVerticalSeparator, disabled, children, footer } = props;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classnames('card__main-actions settings__row', {
|
className={classnames('card__main-actions settings__row', {
|
||||||
|
@ -23,6 +24,7 @@ export default function SettingsRow(props: Props) {
|
||||||
<div className="settings__row--title">
|
<div className="settings__row--title">
|
||||||
<p>{title}</p>
|
<p>{title}</p>
|
||||||
{subtitle && <p className="settings__row--subtitle">{subtitle}</p>}
|
{subtitle && <p className="settings__row--subtitle">{subtitle}</p>}
|
||||||
|
{footer && footer}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classnames('settings__row--value', {
|
className={classnames('settings__row--value', {
|
||||||
|
|
|
@ -43,6 +43,11 @@ const SIDE_LINKS: Array<SideNavLink> = [
|
||||||
section: SETTINGS_GRP.SYSTEM,
|
section: SETTINGS_GRP.SYSTEM,
|
||||||
icon: ICONS.SETTINGS,
|
icon: ICONS.SETTINGS,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Content Hosting',
|
||||||
|
section: SETTINGS_GRP.STORAGE,
|
||||||
|
icon: ICONS.PUBLISH,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function SettingsSideNavigation() {
|
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 DAEMON_STATUS_RECEIVED = 'DAEMON_STATUS_RECEIVED';
|
||||||
export const SHARED_PREFERENCE_SET = 'SHARED_PREFERENCE_SET';
|
export const SHARED_PREFERENCE_SET = 'SHARED_PREFERENCE_SET';
|
||||||
export const SAVE_CUSTOM_WALLET_SERVERS = 'SAVE_CUSTOM_WALLET_SERVERS';
|
export const SAVE_CUSTOM_WALLET_SERVERS = 'SAVE_CUSTOM_WALLET_SERVERS';
|
||||||
|
export const SETTING_DAEMON_SETTINGS = 'SETTING_DAEMON_SETTINGS';
|
||||||
|
|
||||||
// User
|
// User
|
||||||
export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED';
|
export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED';
|
||||||
|
|
|
@ -53,4 +53,5 @@ export const SETTINGS_GRP = {
|
||||||
ACCOUNT: 'account',
|
ACCOUNT: 'account',
|
||||||
CONTENT: 'content',
|
CONTENT: 'content',
|
||||||
SYSTEM: 'system',
|
SYSTEM: 'system',
|
||||||
|
STORAGE: 'Storage',
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@ import SettingAccount from 'component/settingAccount';
|
||||||
import SettingAppearance from 'component/settingAppearance';
|
import SettingAppearance from 'component/settingAppearance';
|
||||||
import SettingContent from 'component/settingContent';
|
import SettingContent from 'component/settingContent';
|
||||||
import SettingSystem from 'component/settingSystem';
|
import SettingSystem from 'component/settingSystem';
|
||||||
|
import SettingStorage from 'component/settingStorage';
|
||||||
|
|
||||||
type DaemonSettings = {
|
type DaemonSettings = {
|
||||||
download_dir: string,
|
download_dir: string,
|
||||||
|
@ -51,6 +52,7 @@ class SettingsPage extends React.PureComponent<Props> {
|
||||||
<SettingAccount />
|
<SettingAccount />
|
||||||
<SettingContent />
|
<SettingContent />
|
||||||
<SettingSystem />
|
<SettingSystem />
|
||||||
|
<SettingStorage />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PrivacyAgreement from 'component/privacyAgreement';
|
import PrivacyAgreement from 'component/privacyAgreement';
|
||||||
|
import HostingSplash from 'component/hostingSplash';
|
||||||
|
import HostingSplashCustom from 'component/hostingSplashCustom';
|
||||||
import WelcomeSplash from 'component/welcomeSplash';
|
import WelcomeSplash from 'component/welcomeSplash';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
const SPLASH_PAGE = 0;
|
const SPLASH_PAGE = 0;
|
||||||
const PRIVACY_PAGE = 1;
|
const PRIVACY_PAGE = 1;
|
||||||
// const HOSTING_PAGE = 2;
|
const HOSTING_PAGE = 2;
|
||||||
// const WELCOME_PAGES = [SPLASH_PAGE, PRIVACY_PAGE];
|
const HOSTING_ADVANCED = 3;
|
||||||
|
|
||||||
type DaemonStatus = {
|
type DaemonStatus = {
|
||||||
disk_space: {
|
disk_space: {
|
||||||
content_blobs_storage_used_mb: string,
|
content_blobs_storage_used_mb: string,
|
||||||
|
@ -44,6 +47,16 @@ export default function Welcome(props: Props) {
|
||||||
const handleNextPage = () => {
|
const handleNextPage = () => {
|
||||||
if (welcomePage === SPLASH_PAGE) {
|
if (welcomePage === SPLASH_PAGE) {
|
||||||
setWelcomePage(PRIVACY_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 (
|
return (
|
||||||
<Page noHeader noSideNavigation>
|
<Page noHeader noSideNavigation>
|
||||||
{welcomePage === SPLASH_PAGE && <WelcomeSplash handleNextPage={handleNextPage} />}
|
{welcomePage === SPLASH_PAGE && <WelcomeSplash handleNextPage={handleNextPage} />}
|
||||||
{welcomePage === PRIVACY_PAGE && <PrivacyAgreement handleNextPage={handleDone} />}
|
{welcomePage === PRIVACY_PAGE && <PrivacyAgreement handleNextPage={handleNextPage} />}
|
||||||
{/* {welcomePage === HOSTING_PAGE && } */}
|
{welcomePage === HOSTING_PAGE && <HostingSplash handleNextPage={handleNextPage} handleDone={handleDone} />}
|
||||||
|
{welcomePage === HOSTING_ADVANCED && (
|
||||||
|
<HostingSplashCustom handleNextPage={handleDone} handleGoBack={handleGoBack} />
|
||||||
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1197,7 +1197,7 @@ export function doFetchModBlockedList() {
|
||||||
if (blockedChannel.blocked_channel_name) {
|
if (blockedChannel.blocked_channel_name) {
|
||||||
const channelUri = buildURI({
|
const channelUri = buildURI({
|
||||||
channelName: blockedChannel.blocked_channel_name,
|
channelName: blockedChannel.blocked_channel_name,
|
||||||
claimId: blockedChannel.blocked_channel_id,
|
channelClaimId: blockedChannel.blocked_channel_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!blockedList.find((blockedChannel) => isURIEqual(blockedChannel.channelUri, channelUri))) {
|
if (!blockedList.find((blockedChannel) => isURIEqual(blockedChannel.channelUri, channelUri))) {
|
||||||
|
@ -1215,7 +1215,7 @@ export function doFetchModBlockedList() {
|
||||||
if (blockedByMap !== undefined) {
|
if (blockedByMap !== undefined) {
|
||||||
const blockedByChannelUri = buildURI({
|
const blockedByChannelUri = buildURI({
|
||||||
channelName: blockedChannel.blocked_by_channel_name,
|
channelName: blockedChannel.blocked_by_channel_name,
|
||||||
claimId: blockedChannel.blocked_by_channel_id,
|
channelClaimId: blockedChannel.blocked_by_channel_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (blockedByMap[channelUri]) {
|
if (blockedByMap[channelUri]) {
|
||||||
|
|
|
@ -104,16 +104,21 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const ready = selectPrefsReady(state);
|
const ready = selectPrefsReady(state);
|
||||||
|
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
return dispatch(doAlertWaitingForSync());
|
return dispatch(doAlertWaitingForSync());
|
||||||
}
|
}
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.SETTING_DAEMON_SETTINGS,
|
||||||
|
data: {
|
||||||
|
val: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
const newSettings = {
|
const newSettings = {
|
||||||
key,
|
key,
|
||||||
value: !value && value !== false ? null : value,
|
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) {
|
if (SDK_SYNC_KEYS.includes(key) && !doNotDispatch) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.SHARED_PREFERENCE_SET,
|
type: ACTIONS.SHARED_PREFERENCE_SET,
|
||||||
|
@ -125,14 +130,33 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
|
||||||
dispatch(doWalletReconnect());
|
dispatch(doWalletReconnect());
|
||||||
// todo: add sdk reloadsettings() (or it happens automagically?)
|
// todo: add sdk reloadsettings() (or it happens automagically?)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.then(() => {
|
||||||
dispatch(doFetchDaemonSettings());
|
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() {
|
export function doCleanBlobs() {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
Lbry.blob_clean().then(() => {
|
return Lbry.blob_clean().then(() => {
|
||||||
dispatch(doFetchDaemonSettings());
|
dispatch(doFetchDaemonSettings());
|
||||||
return 'done';
|
return 'done';
|
||||||
});
|
});
|
||||||
|
|
|
@ -327,10 +327,9 @@ reducers[ACTIONS.DISK_SPACE] = (state, action) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
reducers[ACTIONS.SYNC_STATE_POPULATE] = (state, action) => {
|
reducers[ACTIONS.SYNC_STATE_POPULATE] = (state, action) => {
|
||||||
const { welcomeVersion, allowAnalytics } = action.data;
|
const { allowAnalytics } = action.data;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...(welcomeVersion !== undefined ? { welcomeVersion } : {}),
|
|
||||||
...(allowAnalytics !== undefined ? { allowAnalytics } : {}),
|
...(allowAnalytics !== undefined ? { allowAnalytics } : {}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,6 +14,7 @@ settingLanguage.push('en');
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
isNight: false,
|
isNight: false,
|
||||||
|
isSettingDaemonSettings: false,
|
||||||
findingFFmpeg: false,
|
findingFFmpeg: false,
|
||||||
loadedLanguages: [...Object.keys(window.i18n_messages), 'en'] || ['en'],
|
loadedLanguages: [...Object.keys(window.i18n_messages), 'en'] || ['en'],
|
||||||
customWalletServers: [],
|
customWalletServers: [],
|
||||||
|
@ -93,6 +94,11 @@ reducers[ACTIONS.FINDING_FFMPEG_STARTED] = (state) =>
|
||||||
findingFFmpeg: true,
|
findingFFmpeg: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.SETTING_DAEMON_SETTINGS] = (state, action) =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
isSettingDaemonSettings: action.data.val,
|
||||||
|
});
|
||||||
|
|
||||||
reducers[ACTIONS.FINDING_FFMPEG_COMPLETED] = (state) =>
|
reducers[ACTIONS.FINDING_FFMPEG_COMPLETED] = (state) =>
|
||||||
Object.assign({}, state, {
|
Object.assign({}, state, {
|
||||||
findingFFmpeg: false,
|
findingFFmpeg: false,
|
||||||
|
|
|
@ -9,10 +9,23 @@ const homepages = require('homepages');
|
||||||
const selectState = (state) => state.settings || {};
|
const selectState = (state) => state.settings || {};
|
||||||
|
|
||||||
export const selectDaemonSettings = createSelector(selectState, (state) => state.daemonSettings);
|
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 selectDaemonStatus = createSelector(selectState, (state) => state.daemonStatus);
|
||||||
|
|
||||||
export const selectFfmpegStatus = createSelector(selectDaemonStatus, (status) => status.ffmpeg_status);
|
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);
|
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 selectosNotificationsEnabled = makeSelectClientSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED);
|
||||||
|
|
||||||
export const selectDisableAutoUpdates = makeSelectClientSetting(SETTINGS.DISABLE_AUTO_UPDATES);
|
export const selectDisableAutoUpdates = makeSelectClientSetting(SETTINGS.DISABLE_AUTO_UPDATES);
|
||||||
|
|
|
@ -172,17 +172,28 @@ input-submit {
|
||||||
margin: 0;
|
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 {
|
& > *:first-child {
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-bottom-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;
|
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,
|
.checkbox,
|
||||||
|
|
|
@ -548,6 +548,7 @@ body {
|
||||||
border-top: unset;
|
border-top: unset;
|
||||||
|
|
||||||
.settings__row {
|
.settings__row {
|
||||||
|
align-items: flex-start;
|
||||||
padding: var(--spacing-s);
|
padding: var(--spacing-s);
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
.checkbox {
|
.checkbox {
|
||||||
|
|
|
@ -29,3 +29,95 @@
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding-top: var(--spacing-m);
|
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 {
|
&:only-child {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
&.section__actions--between {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__main-actions.settings__row {
|
.card__main-actions.settings__row {
|
||||||
|
@ -301,6 +304,7 @@
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
padding-bottom: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings__row--title {
|
.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
|
// Temporary master classes
|
||||||
.date_time {
|
.date_time {
|
||||||
font-size: var(--font-xsmall);
|
font-size: var(--font-xsmall);
|
||||||
|
|
|
@ -200,4 +200,13 @@
|
||||||
radial-gradient(circle at 50% 117%, rgba(25, 25, 25, 0.2) 0, #202020 100%);
|
radial-gradient(circle at 50% 117%, rgba(25, 25, 25, 0.2) 0, #202020 100%);
|
||||||
|
|
||||||
--mui-background: #000;
|
--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
|
// Input
|
||||||
--color-input-bg-selected: var(--color-primary-alt);
|
--color-input-bg-selected: var(--color-primary-alt);
|
||||||
--color-input-color: #111111;
|
--color-input-color: #111111;
|
||||||
--color-input-label: var(--color-gray-5);
|
--color-input-label: var(--color-text-base);
|
||||||
--color-input-placeholder: #212529;
|
--color-input-placeholder: #212529;
|
||||||
--color-input-bg: var(--color-white);
|
--color-input-bg: var(--color-white);
|
||||||
--color-input-border: var(--color-border);
|
--color-input-border: var(--color-border);
|
||||||
|
@ -206,4 +206,13 @@
|
||||||
|
|
||||||
--mui-background: #fff;
|
--mui-background: #fff;
|
||||||
--mui-button: var(--color-header-button);
|
--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