add hosting to first run

This commit is contained in:
zeppi 2022-06-01 15:27:47 -04:00
parent 6108860063
commit a18dba595b
36 changed files with 990 additions and 222 deletions

View file

@ -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.3
# STRIPE
# STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo'

View file

@ -2315,8 +2315,18 @@
"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",
"* Note that as\n peer-to-peer software, your IP address and potentially other system information can be sent to other\n users, though this information is not stored permanently.": "* Note that as\n peer-to-peer software, your IP address and potentially other system information can be sent to other\n users, though this information is not stored permanently.",
"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 downloads a small slice of content currently active on the network.": "Automatic Hosting downloads a small slice of content currently active on the network.",
"Automatic Hosting (GB)": "Automatic Hosting (GB)",
"--end--": "--end--"
}

View 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);

View file

@ -0,0 +1,107 @@
// @flow
import * as React from 'react';
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</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</label>
<div className={'help'}>
{autoHostingLimit === 0 ? __('Disabled') : `${getGB(autoBlobSpace)} of ${getGB(autoHostingLimit)} GB`}
</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</label>
<div className={'help'}>
{viewHostingLimit === 1
? __('Disabled')
: `${getGB(viewBlobSpace)} of ${
viewHostingLimit !== 0 ? getGB(viewHostingLimit) : `${getGB(viewFree)} Free`
} GB`}
</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</label>
<div className={'help'}>{`${getGB(unallocFree)} GB`}</div>
</div>
</div>
)}
</div>
</div>
);
}
export default StorageViz;

View 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);

View file

@ -0,0 +1,153 @@
// @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,
};
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 * 0.2 // lots of free space?
? blobSpaceUsed > totalMB * 0.1 // using more than 10%?
? (freeMB + blobSpaceUsed) / 2 // e.g. 40g used plus 30g free, knock back to 35g limit, freeing to 35g
: totalMB * 0.1 // 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);

View file

@ -0,0 +1,3 @@
import HostingSplashCustom from './view';
export default HostingSplashCustom;

View file

@ -0,0 +1,35 @@
// @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;
function handleSubmit() {
handleNextPage();
}
return (
<section className="main--contained">
<div className={'first-run__wrapper'}>
<SettingStorage isWelcome />
<Form onSubmit={handleSubmit} 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);

View file

@ -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">

View file

@ -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) => ({

View file

@ -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>
)
name="network_blob_limit_gb"
type="number"
label={__(`Automatic Hosting (GB)`)}
disabled={!daemonSettings.save_blobs || isSetting}
onKeyDown={handleKeyDown}
inputButton={
<>
<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={handleApply}
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}
/>
</>
}
/>
</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'}>
<Button
disabled={
(unlimited && blobLimitSetting === '0') ||
(!unlimited &&
(blobLimitSetting === convertGbToMb(contentBlobSpaceLimitGB) || // &&
// networkLimitSetting === convertGbToMb(networkBlobSpaceLimitGB)
!validHostingAmount(String(contentBlobSpaceLimitGB)))) ||
applying
}
type="button"
button="primary"
onClick={handleApply}
label={__('Apply')}
onChange={(e) => handleNetworkLimitChange(e.target.value)}
value={networkBlobSpaceLimitGB}
/>
</div>
</>

View 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);

View 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;

View 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);

View 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 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 helping 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 slice of content currently active on the network.')}{' '}
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/host-content" />
</React.Fragment>
}
>
<SettingDataHosting />
</SettingsRow>
</>
}
/>
</>
);
}

View file

@ -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={

View 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);

View file

@ -0,0 +1,143 @@
// @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 ---
viewHostingLimit: number,
disabled?: boolean,
isSetting: boolean,
// --- perform ---
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
cleanBlobs: () => string,
getDaemonStatus: () => void,
};
function SettingViewHosting(props: Props) {
const { viewHostingLimit, setDaemonSetting, cleanBlobs, getDaemonStatus, disabled, isSetting } = props;
// daemon settings come in as 'number', but we manage them as 'String'.
const [contentBlobSpaceLimitGB, setContentBlobSpaceLimit] = React.useState(
viewHostingLimit === 0 ? '0.01' : String(viewHostingLimit / 1024)
);
const [unlimited, setUnlimited] = React.useState(viewHostingLimit === 0);
function handleContentLimitChange(gb) {
if (gb === '') {
setContentBlobSpaceLimit('');
} else if (gb === '0') {
setContentBlobSpaceLimit('0.01'); // 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,
String(contentBlobSpaceLimitGB === '0.01' ? '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 === '0') ||
(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 === '0') ||
(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('0.01') ? '0' : contentBlobSpaceLimitGB}
/>
</div>
</>
);
}
export default SettingViewHosting;

View file

@ -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', {

View file

@ -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() {

View file

@ -248,6 +248,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';

View file

@ -53,4 +53,5 @@ export const SETTINGS_GRP = {
ACCOUNT: 'account',
CONTENT: 'content',
SYSTEM: 'system',
STORAGE: 'Storage',
};

View file

@ -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>

View file

@ -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>
);
}

View file

@ -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]) {

View file

@ -104,35 +104,59 @@ 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) => {
if (SDK_SYNC_KEYS.includes(key) && !doNotDispatch) {
Lbry.settings_set(newSettings)
.then((newSetting) => {
if (SDK_SYNC_KEYS.includes(key) && !doNotDispatch) {
dispatch({
type: ACTIONS.SHARED_PREFERENCE_SET,
data: { key: key, value: newSetting[key] },
});
}
// hardcoding this in lieu of a better solution
if (key === DAEMON_SETTINGS.LBRYUM_SERVERS) {
dispatch(doWalletReconnect());
// todo: add sdk reloadsettings() (or it happens automagically?)
}
})
.then(() => {
dispatch(doFetchDaemonSettings());
})
.then(() => {
dispatch({
type: ACTIONS.SHARED_PREFERENCE_SET,
data: { key: key, value: newSetting[key] },
type: ACTIONS.SETTING_DAEMON_SETTINGS,
data: {
val: false,
},
});
}
// hardcoding this in lieu of a better solution
if (key === DAEMON_SETTINGS.LBRYUM_SERVERS) {
dispatch(doWalletReconnect());
// todo: add sdk reloadsettings() (or it happens automagically?)
}
});
dispatch(doFetchDaemonSettings());
})
.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';
});

View file

@ -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,

View file

@ -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);

View file

@ -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,

View file

@ -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 {

View file

@ -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);
}
}
}
}

View file

@ -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;
}
margin-bottom: var(--spacing-m);
}
.settings__row--title {

View file

@ -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);

View file

@ -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;
}

View file

@ -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
View 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);
}