remove web and embed
This commit is contained in:
parent
b50779f1e5
commit
7d5d7d3c55
57 changed files with 65 additions and 11915 deletions
|
@ -24,7 +24,6 @@ import PdfViewer from 'component/viewers/pdfViewer';
|
|||
type Props = {
|
||||
uri: string,
|
||||
streamingUrl: string,
|
||||
embedded?: boolean,
|
||||
contentType: string,
|
||||
claim: StreamClaim,
|
||||
currentTheme: string,
|
||||
|
@ -45,9 +44,8 @@ class FileRender extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { embedded } = this.props;
|
||||
window.addEventListener('keydown', this.escapeListener, true);
|
||||
analytics.playerLoadedEvent(embedded);
|
||||
analytics.playerLoadedEvent();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -146,13 +144,12 @@ class FileRender extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { embedded, renderMode, className } = this.props;
|
||||
const { renderMode, className } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('file-render', className, {
|
||||
'file-render--document': RENDER_MODES.TEXT_MODES.includes(renderMode) && !embedded,
|
||||
'file-render--embed': embedded,
|
||||
'file-render--document': RENDER_MODES.TEXT_MODES.includes(renderMode),
|
||||
'file-render--video': renderMode === RENDER_MODES.VIDEO || renderMode === RENDER_MODES.AUDIO,
|
||||
})}
|
||||
>
|
||||
|
|
|
@ -37,7 +37,6 @@ import ChannelsPage from 'page/channels';
|
|||
import CheckoutPage from 'page/checkoutPage';
|
||||
import CreatorDashboard from 'page/creatorDashboard';
|
||||
import DiscoverPage from 'page/discover';
|
||||
import EmbedWrapperPage from 'page/embedWrapper';
|
||||
import FileListPublished from 'page/fileListPublished';
|
||||
import FourOhFourPage from 'page/fourOhFour';
|
||||
import HelpPage from 'page/help';
|
||||
|
@ -308,9 +307,6 @@ function AppRouter(props: Props) {
|
|||
<PrivateRoute {...props} path={`/$/${PAGES.AUTH_WALLET_PASSWORD}`} component={SignInWalletPasswordPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_OWN_COMMENTS}`} component={OwnComments} />
|
||||
|
||||
<Route path={`/$/${PAGES.EMBED}/:claimName`} exact component={EmbedWrapperPage} />
|
||||
<Route path={`/$/${PAGES.EMBED}/:claimName/:claimId`} exact component={EmbedWrapperPage} />
|
||||
|
||||
{/* Below need to go at the end to make sure we don't match any of our pages first */}
|
||||
<Route path="/:claimName" exact component={ShowPage} />
|
||||
<Route path="/:claimName/:streamName" exact component={ShowPage} />
|
||||
|
|
|
@ -22,12 +22,11 @@ import { withRouter } from 'react-router';
|
|||
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
||||
import { selectDaemonSettings, makeSelectClientSetting, selectHomepageData } from 'redux/selectors/settings';
|
||||
import { toggleVideoTheaterMode, toggleAutoplayNext, doSetClientSetting } from 'redux/actions/settings';
|
||||
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
|
||||
import { selectUser } from 'redux/selectors/user';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { search } = props.location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const autoplay = urlParams.get('autoplay');
|
||||
const uri = props.uri;
|
||||
// TODO: eventually this should be received from DB and not local state (https://github.com/lbryio/lbry-desktop/issues/6796)
|
||||
const position = urlParams.get('t') !== null ? urlParams.get('t') : makeSelectContentPositionForUri(uri)(state);
|
||||
|
@ -53,7 +52,6 @@ const select = (state, props) => {
|
|||
nextRecommendedUri,
|
||||
previousListUri,
|
||||
isMarkdownOrComment,
|
||||
autoplayIfEmbedded: Boolean(autoplay),
|
||||
autoplayNext: makeSelectClientSetting(SETTINGS.AUTOPLAY_NEXT)(state),
|
||||
volume: selectVolume(state),
|
||||
muted: selectMute(state),
|
||||
|
@ -61,7 +59,6 @@ const select = (state, props) => {
|
|||
thumbnail: makeSelectThumbnailForUri(uri)(state),
|
||||
claim: makeSelectClaimForUri(uri)(state),
|
||||
homepageData: selectHomepageData(state),
|
||||
authenticated: selectUserVerifiedEmail(state),
|
||||
shareTelemetry: IS_WEB || selectDaemonSettings(state).share_usage_data,
|
||||
isFloating: makeSelectIsPlayerFloating(props.location)(state),
|
||||
videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state),
|
||||
|
|
|
@ -48,12 +48,9 @@ type Props = {
|
|||
poster: ?string,
|
||||
onPlayerReady: (Player, any) => void,
|
||||
isAudio: boolean,
|
||||
startMuted: boolean,
|
||||
autoplay: boolean,
|
||||
autoplaySetting: boolean,
|
||||
embedded: boolean,
|
||||
toggleVideoTheaterMode: () => void,
|
||||
adUrl: ?string,
|
||||
claimId: ?string,
|
||||
userId: ?number,
|
||||
// allowPreRoll: ?boolean,
|
||||
|
@ -163,18 +160,14 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
const {
|
||||
autoplay,
|
||||
autoplaySetting,
|
||||
embedded,
|
||||
startMuted,
|
||||
source,
|
||||
sourceType,
|
||||
poster,
|
||||
isAudio,
|
||||
onPlayerReady,
|
||||
toggleVideoTheaterMode,
|
||||
adUrl,
|
||||
claimId,
|
||||
userId,
|
||||
// allowPreRoll,
|
||||
shareTelemetry,
|
||||
replay,
|
||||
videoTheaterMode,
|
||||
|
@ -188,7 +181,6 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
const videoJsOptions = {
|
||||
...VIDEO_JS_OPTIONS,
|
||||
autoplay: autoplay,
|
||||
muted: startMuted,
|
||||
sources: [
|
||||
{
|
||||
src: source,
|
||||
|
@ -361,10 +353,9 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
}
|
||||
|
||||
const onEnded = React.useCallback(() => {
|
||||
if (!adUrl) {
|
||||
showTapButton(TAP.NONE);
|
||||
}
|
||||
}, [adUrl]);
|
||||
// not sure if this is necessary - used to be dependent on !adUrl
|
||||
showTapButton(TAP.NONE);
|
||||
}, []);
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
const player = playerRef.current;
|
||||
|
@ -565,7 +556,6 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
player.recsys({
|
||||
videoId: claimId,
|
||||
userId: userId,
|
||||
embedded: embedded,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,29 +1,19 @@
|
|||
// @flow
|
||||
import { ENABLE_PREROLL_ADS } from 'config';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React, { useEffect, useState, useContext, useCallback } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { stopContextMenu } from 'util/context-menu';
|
||||
import type { Player } from './internal/videojs';
|
||||
import VideoJs from './internal/videojs';
|
||||
import analytics from 'analytics';
|
||||
import { EmbedContext } from 'page/embedWrapper/view';
|
||||
import classnames from 'classnames';
|
||||
import { FORCE_CONTENT_TYPE_PLAYER } from 'constants/claim';
|
||||
import AutoplayCountdown from 'component/autoplayCountdown';
|
||||
import usePrevious from 'effects/use-previous';
|
||||
import FileViewerEmbeddedEnded from 'web/component/fileViewerEmbeddedEnded';
|
||||
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
|
||||
import LoadingScreen from 'component/common/loading-screen';
|
||||
import { addTheaterModeButton } from './internal/theater-mode';
|
||||
import { addAutoplayNextButton } from './internal/autoplay-next';
|
||||
import { addPlayNextButton } from './internal/play-next';
|
||||
import { addPlayPreviousButton } from './internal/play-previous';
|
||||
import { useGetAds } from 'effects/use-get-ads';
|
||||
import Button from 'component/button';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import { useHistory } from 'react-router';
|
||||
import { getAllIds } from 'util/buildHomepage';
|
||||
import type { HomepageCat } from 'util/buildHomepage';
|
||||
import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
|
||||
|
||||
|
@ -43,7 +33,6 @@ type Props = {
|
|||
volume: number,
|
||||
uri: string,
|
||||
autoplayNext: boolean,
|
||||
autoplayIfEmbedded: boolean,
|
||||
desktopPlayStartTime?: number,
|
||||
doAnalyticsView: (string, number) => Promise<any>,
|
||||
doAnalyticsBuffer: (string, any) => void,
|
||||
|
@ -85,7 +74,6 @@ function VideoViewer(props: Props) {
|
|||
muted,
|
||||
volume,
|
||||
autoplayNext,
|
||||
autoplayIfEmbedded,
|
||||
doAnalyticsView,
|
||||
doAnalyticsBuffer,
|
||||
claimRewards,
|
||||
|
@ -95,8 +83,6 @@ function VideoViewer(props: Props) {
|
|||
toggleVideoTheaterMode,
|
||||
toggleAutoplayNext,
|
||||
setVideoPlaybackRate,
|
||||
homepageData,
|
||||
authenticated,
|
||||
userId,
|
||||
shareTelemetry,
|
||||
isFloating,
|
||||
|
@ -108,27 +94,17 @@ function VideoViewer(props: Props) {
|
|||
isMarkdownOrComment,
|
||||
} = props;
|
||||
const permanentUrl = claim && claim.permanent_url;
|
||||
const adApprovedChannelIds = homepageData ? getAllIds(homepageData) : [];
|
||||
const claimId = claim && claim.claim_id;
|
||||
const channelClaimId = claim && claim.signing_channel && claim.signing_channel.claim_id;
|
||||
const isAudio = contentType.includes('audio');
|
||||
const forcePlayer = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
|
||||
const {
|
||||
push,
|
||||
location: { pathname },
|
||||
} = useHistory();
|
||||
const { push } = useHistory();
|
||||
const [doNavigate, setDoNavigate] = useState(false);
|
||||
const [playNextUrl, setPlayNextUrl] = useState(true);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [ended, setEnded] = useState(false);
|
||||
const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false);
|
||||
const [isEndedEmbed, setIsEndedEmbed] = useState(false);
|
||||
const vjsCallbackDataRef: any = React.useRef();
|
||||
const previousUri = usePrevious(uri);
|
||||
const embedded = useContext(EmbedContext);
|
||||
const approvedVideo = Boolean(channelClaimId) && adApprovedChannelIds.includes(channelClaimId);
|
||||
const adsEnabled = ENABLE_PREROLL_ADS && !authenticated && !embedded && approvedVideo;
|
||||
const [adUrl, setAdUrl, isFetchingAd] = useGetAds(approvedVideo, adsEnabled);
|
||||
/* isLoading was designed to show loading screen on first play press, rather than completely black screen, but
|
||||
breaks because some browsers (e.g. Firefox) block autoplay but leave the player.play Promise pending */
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
@ -139,7 +115,6 @@ function VideoViewer(props: Props) {
|
|||
useEffect(() => {
|
||||
if (uri && previousUri && uri !== previousUri) {
|
||||
setShowAutoplayCountdown(false);
|
||||
setIsEndedEmbed(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [uri, previousUri]);
|
||||
|
@ -147,10 +122,9 @@ function VideoViewer(props: Props) {
|
|||
// Update vjsCallbackDataRef (ensures videojs callbacks are not using stale values):
|
||||
useEffect(() => {
|
||||
vjsCallbackDataRef.current = {
|
||||
embedded: embedded,
|
||||
videoPlaybackRate: videoPlaybackRate,
|
||||
};
|
||||
}, [embedded, videoPlaybackRate]);
|
||||
}, [videoPlaybackRate]);
|
||||
|
||||
function doTrackingBuffered(e: Event, data: any) {
|
||||
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
|
||||
|
@ -166,7 +140,7 @@ function VideoViewer(props: Props) {
|
|||
const differenceToAdd = Date.now() - desktopPlayStartTime;
|
||||
timeToStart += differenceToAdd;
|
||||
}
|
||||
analytics.playerStartedEvent(embedded);
|
||||
analytics.playerStartedEvent();
|
||||
|
||||
// convert bytes to bits, and then divide by seconds
|
||||
const contentInBits = Number(claim.value.source.size) * 8;
|
||||
|
@ -178,7 +152,15 @@ function VideoViewer(props: Props) {
|
|||
|
||||
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
|
||||
let playerPoweredBy = response.headers.get('x-powered-by') || '';
|
||||
analytics.videoStartEvent(claimId, timeToStart, playerPoweredBy, userId, claim.canonical_url, this, bitrateAsBitsPerSecond);
|
||||
analytics.videoStartEvent(
|
||||
claimId,
|
||||
timeToStart,
|
||||
playerPoweredBy,
|
||||
userId,
|
||||
claim.canonical_url,
|
||||
this,
|
||||
bitrateAsBitsPerSecond
|
||||
);
|
||||
});
|
||||
|
||||
doAnalyticsView(uri, timeToStart).then(() => {
|
||||
|
@ -248,28 +230,20 @@ function VideoViewer(props: Props) {
|
|||
|
||||
analytics.videoIsPlaying(false);
|
||||
|
||||
if (adUrl) {
|
||||
setAdUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (embedded) {
|
||||
setIsEndedEmbed(true);
|
||||
} else if (!collectionId && autoplayNext) {
|
||||
if (!collectionId && autoplayNext) {
|
||||
setShowAutoplayCountdown(true);
|
||||
} else if (collectionId) {
|
||||
setDoNavigate(true);
|
||||
}
|
||||
|
||||
clearPosition(uri);
|
||||
}, [adUrl, autoplayNext, clearPosition, collectionId, embedded, ended, setAdUrl, uri]);
|
||||
}, [autoplayNext, clearPosition, collectionId, ended, uri]);
|
||||
|
||||
function onPlay(player) {
|
||||
setEnded(false);
|
||||
setIsLoading(false);
|
||||
setIsPlaying(true);
|
||||
setShowAutoplayCountdown(false);
|
||||
setIsEndedEmbed(false);
|
||||
setReplay(false);
|
||||
setDoNavigate(false);
|
||||
analytics.videoIsPlaying(true, player);
|
||||
|
@ -296,7 +270,7 @@ function VideoViewer(props: Props) {
|
|||
}
|
||||
}
|
||||
|
||||
const playerReadyDependencyList = [uri, adUrl, embedded, autoplayIfEmbedded];
|
||||
const playerReadyDependencyList = [uri];
|
||||
if (!IS_WEB) {
|
||||
playerReadyDependencyList.push(desktopPlayStartTime);
|
||||
}
|
||||
|
@ -312,43 +286,38 @@ function VideoViewer(props: Props) {
|
|||
};
|
||||
|
||||
const onPlayerReady = useCallback((player: Player, videoNode: any) => {
|
||||
if (!embedded) {
|
||||
setVideoNode(videoNode);
|
||||
player.muted(muted);
|
||||
player.volume(volume);
|
||||
player.playbackRate(videoPlaybackRate);
|
||||
if (!isMarkdownOrComment) {
|
||||
addTheaterModeButton(player, toggleVideoTheaterMode);
|
||||
if (collectionId) {
|
||||
addPlayNextButton(player, doPlayNext);
|
||||
addPlayPreviousButton(player, doPlayPrevious);
|
||||
} else {
|
||||
addAutoplayNextButton(player, toggleAutoplayNext, autoplayNext);
|
||||
}
|
||||
setVideoNode(videoNode);
|
||||
player.muted(muted);
|
||||
player.volume(volume);
|
||||
player.playbackRate(videoPlaybackRate);
|
||||
if (!isMarkdownOrComment) {
|
||||
addTheaterModeButton(player, toggleVideoTheaterMode);
|
||||
if (collectionId) {
|
||||
addPlayNextButton(player, doPlayNext);
|
||||
addPlayPreviousButton(player, doPlayPrevious);
|
||||
} else {
|
||||
addAutoplayNextButton(player, toggleAutoplayNext, autoplayNext);
|
||||
}
|
||||
}
|
||||
|
||||
const shouldPlay = !embedded || autoplayIfEmbedded;
|
||||
// https://blog.videojs.com/autoplay-best-practices-with-video-js/#Programmatic-Autoplay-and-Success-Failure-Detection
|
||||
if (shouldPlay) {
|
||||
const playPromise = player.play();
|
||||
const timeoutPromise = new Promise((resolve, reject) =>
|
||||
setTimeout(() => reject(PLAY_TIMEOUT_ERROR), PLAY_TIMEOUT_LIMIT)
|
||||
);
|
||||
const playPromise = player.play();
|
||||
const timeoutPromise = new Promise((resolve, reject) =>
|
||||
setTimeout(() => reject(PLAY_TIMEOUT_ERROR), PLAY_TIMEOUT_LIMIT)
|
||||
);
|
||||
|
||||
Promise.race([playPromise, timeoutPromise]).catch((error) => {
|
||||
if (typeof error === 'object' && error.name && error.name === 'NotAllowedError') {
|
||||
if (player.autoplay() && !player.muted()) {
|
||||
// player.muted(true);
|
||||
// another version had player.play()
|
||||
}
|
||||
Promise.race([playPromise, timeoutPromise]).catch((error) => {
|
||||
if (typeof error === 'object' && error.name && error.name === 'NotAllowedError') {
|
||||
if (player.autoplay() && !player.muted()) {
|
||||
// player.muted(true);
|
||||
// another version had player.play()
|
||||
}
|
||||
setIsLoading(false);
|
||||
setIsPlaying(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
setIsPlaying(false);
|
||||
});
|
||||
|
||||
setIsLoading(shouldPlay); // if we are here outside of an embed, we're playing
|
||||
setIsLoading(true); // if we are here outside of an embed, we're playing
|
||||
|
||||
// PR: #5535
|
||||
// Move the restoration to a later `loadedmetadata` phase to counter the
|
||||
|
@ -398,7 +367,6 @@ function VideoViewer(props: Props) {
|
|||
<div
|
||||
className={classnames('file-viewer', {
|
||||
'file-viewer--is-playing': isPlaying,
|
||||
'file-viewer--ended-embed': isEndedEmbed,
|
||||
})}
|
||||
onContextMenu={stopContextMenu}
|
||||
>
|
||||
|
@ -409,64 +377,25 @@ function VideoViewer(props: Props) {
|
|||
doReplay={() => setReplay(true)}
|
||||
/>
|
||||
)}
|
||||
{isEndedEmbed && <FileViewerEmbeddedEnded uri={uri} />}
|
||||
{embedded && !isEndedEmbed && <FileViewerEmbeddedTitle uri={uri} />}
|
||||
{/* disable this loading behavior because it breaks when player.play() promise hangs */}
|
||||
{isLoading && <LoadingScreen status={__('Loading')} />}
|
||||
|
||||
{!isFetchingAd && adUrl && (
|
||||
<>
|
||||
<span className="ads__video-notify">
|
||||
{__('Advertisement')}{' '}
|
||||
<Button
|
||||
className="ads__video-close"
|
||||
icon={ICONS.REMOVE}
|
||||
title={__('Close')}
|
||||
onClick={() => setAdUrl(null)}
|
||||
/>
|
||||
</span>
|
||||
<span className="ads__video-nudge">
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
sign_up: (
|
||||
<Button
|
||||
button="secondary"
|
||||
className="ads__video-link"
|
||||
label={__('Sign Up')}
|
||||
navigate={`/$/${PAGES.AUTH}?redirect=${pathname}&src=video-ad`}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
%sign_up% to turn ads off.
|
||||
</I18nMessage>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isFetchingAd && (
|
||||
<VideoJs
|
||||
adUrl={adUrl}
|
||||
source={adUrl || source}
|
||||
sourceType={forcePlayer || adUrl ? 'video/mp4' : contentType}
|
||||
isAudio={isAudio}
|
||||
poster={isAudio || (embedded && !autoplayIfEmbedded) ? thumbnail : ''}
|
||||
onPlayerReady={onPlayerReady}
|
||||
startMuted={autoplayIfEmbedded}
|
||||
toggleVideoTheaterMode={toggleVideoTheaterMode}
|
||||
autoplay={!embedded || autoplayIfEmbedded}
|
||||
autoplaySetting={autoplayNext}
|
||||
claimId={claimId}
|
||||
userId={userId}
|
||||
allowPreRoll={!embedded && !authenticated}
|
||||
shareTelemetry={shareTelemetry}
|
||||
replay={replay}
|
||||
videoTheaterMode={videoTheaterMode}
|
||||
playNext={doPlayNext}
|
||||
playPrevious={doPlayPrevious}
|
||||
embedded={embedded}
|
||||
/>
|
||||
)}
|
||||
<VideoJs
|
||||
source={source}
|
||||
sourceType={forcePlayer ? 'video/mp4' : contentType}
|
||||
isAudio={isAudio}
|
||||
poster={isAudio ? thumbnail : ''}
|
||||
onPlayerReady={onPlayerReady}
|
||||
toggleVideoTheaterMode={toggleVideoTheaterMode}
|
||||
autoplay
|
||||
autoplaySetting={autoplayNext}
|
||||
claimId={claimId}
|
||||
userId={userId}
|
||||
shareTelemetry={shareTelemetry}
|
||||
replay={replay}
|
||||
videoTheaterMode={videoTheaterMode}
|
||||
playNext={doPlayNext}
|
||||
playPrevious={doPlayPrevious}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import EmbedWrapperPage from './view';
|
||||
import { makeSelectClaimForUri, makeSelectIsUriResolving } from 'redux/selectors/claims';
|
||||
import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
|
||||
import { doResolveUri } from 'redux/actions/claims';
|
||||
import { buildURI } from 'util/lbryURI';
|
||||
import { doPlayUri } from 'redux/actions/content';
|
||||
import { makeSelectCostInfoForUri, doFetchCostInfoForUri, selectBlackListedOutpoints } from 'lbryinc';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { match } = props;
|
||||
const { params } = match;
|
||||
const { claimName, claimId } = params;
|
||||
const uri = claimName ? buildURI({ claimName, claimId }) : '';
|
||||
return {
|
||||
uri,
|
||||
claim: makeSelectClaimForUri(uri)(state),
|
||||
costInfo: makeSelectCostInfoForUri(uri)(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
|
||||
isResolvingUri: makeSelectIsUriResolving(uri)(state),
|
||||
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => {
|
||||
return {
|
||||
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||
doPlayUri: (uri) => dispatch(doPlayUri(uri)),
|
||||
doFetchCostInfoForUri: (uri) => dispatch(doFetchCostInfoForUri(uri)),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select, perform)(EmbedWrapperPage);
|
|
@ -1,126 +0,0 @@
|
|||
// @flow
|
||||
import { SITE_NAME } from 'config';
|
||||
import React, { useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import FileRender from 'component/fileRender';
|
||||
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
|
||||
import Spinner from 'component/spinner';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
resolveUri: (string) => void,
|
||||
claim: Claim,
|
||||
doPlayUri: (string) => void,
|
||||
costInfo: any,
|
||||
streamingUrl: string,
|
||||
doFetchCostInfoForUri: (string) => void,
|
||||
isResolvingUri: boolean,
|
||||
blackListedOutpoints: Array<{
|
||||
txid: string,
|
||||
nout: number,
|
||||
}>,
|
||||
};
|
||||
|
||||
export const EmbedContext = React.createContext<any>();
|
||||
const EmbedWrapperPage = (props: Props) => {
|
||||
const {
|
||||
resolveUri,
|
||||
claim,
|
||||
uri,
|
||||
doPlayUri,
|
||||
costInfo,
|
||||
streamingUrl,
|
||||
doFetchCostInfoForUri,
|
||||
isResolvingUri,
|
||||
blackListedOutpoints,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
location: { search },
|
||||
} = useHistory();
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const embedLightBackground = urlParams.get('embedBackgroundLight');
|
||||
const haveClaim = Boolean(claim);
|
||||
const readyToDisplay = claim && streamingUrl;
|
||||
const loading = !claim && isResolvingUri;
|
||||
const noContentFound = !claim && !isResolvingUri;
|
||||
const isPaidContent = costInfo && costInfo.cost > 0;
|
||||
const contentLink = formatLbryUrlForWeb(uri);
|
||||
const signingChannel = claim && claim.signing_channel;
|
||||
const isClaimBlackListed =
|
||||
claim &&
|
||||
blackListedOutpoints &&
|
||||
blackListedOutpoints.some(
|
||||
(outpoint) =>
|
||||
(signingChannel && outpoint.txid === signingChannel.txid && outpoint.nout === signingChannel.nout) ||
|
||||
(outpoint.txid === claim.txid && outpoint.nout === claim.nout)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (resolveUri && uri && !haveClaim) {
|
||||
resolveUri(uri);
|
||||
}
|
||||
if (uri && haveClaim && costInfo && costInfo.cost === 0) {
|
||||
doPlayUri(uri);
|
||||
}
|
||||
}, [resolveUri, uri, doPlayUri, haveClaim, costInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (haveClaim && uri && doFetchCostInfoForUri) {
|
||||
doFetchCostInfoForUri(uri);
|
||||
}
|
||||
}, [uri, haveClaim, doFetchCostInfoForUri]);
|
||||
|
||||
if (isClaimBlackListed) {
|
||||
return (
|
||||
<Card
|
||||
title={uri}
|
||||
subtitle={__(
|
||||
'In response to a complaint we received under the US Digital Millennium Copyright Act, we have blocked access to this content from our applications.'
|
||||
)}
|
||||
actions={
|
||||
<div className="section__actions">
|
||||
<Button button="link" href="https://lbry.com/faq/dmca" label={__('Read More')} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('embed__wrapper', {
|
||||
'embed__wrapper--light-background': embedLightBackground,
|
||||
})}
|
||||
>
|
||||
<EmbedContext.Provider value>
|
||||
{readyToDisplay ? (
|
||||
<FileRender uri={uri} embedded />
|
||||
) : (
|
||||
<div className="embed__loading">
|
||||
<FileViewerEmbeddedTitle uri={uri} />
|
||||
|
||||
<div className="embed__loading-text">
|
||||
{loading && <Spinner delayed light />}
|
||||
{noContentFound && <h1>{__('No content found.')}</h1>}
|
||||
{isPaidContent && (
|
||||
<div>
|
||||
<h1>{__('Paid content cannot be embedded.')}</h1>
|
||||
<div className="section__actions--centered">
|
||||
<Button label={__('Watch on %SITE_NAME%', { SITE_NAME })} button="primary" href={contentLink} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</EmbedContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmbedWrapperPage;
|
|
@ -20,7 +20,6 @@
|
|||
@import 'component/comments';
|
||||
@import 'component/content';
|
||||
@import 'component/dat-gui';
|
||||
@import 'component/embed-player';
|
||||
@import 'component/expandable';
|
||||
@import 'component/expanding-details';
|
||||
@import 'component/file-drop';
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
.embed__wrapper {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--color-black);
|
||||
}
|
||||
|
||||
.embed__wrapper--light-background {
|
||||
@extend .embed__wrapper;
|
||||
|
||||
.vjs-poster,
|
||||
video {
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.embed__inline-button {
|
||||
@include thumbnail;
|
||||
position: relative;
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
background-color: var(--color-black);
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.embed__inline-button-preview {
|
||||
@extend .embed__inline-button;
|
||||
background-color: var(--color-editor-inline-code-bg);
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.embed__loading {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.embed__loading-text {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-white);
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
}
|
||||
|
||||
.embed__overlay-logo {
|
||||
max-height: 2rem;
|
||||
max-width: 7rem;
|
||||
}
|
92
web/.env.ody
92
web/.env.ody
|
@ -1,92 +0,0 @@
|
|||
# Copy this file to .env to make modifications
|
||||
|
||||
# Base config
|
||||
|
||||
WEBPACK_WEB_PORT=9090
|
||||
WEBPACK_ELECTRON_PORT=9091
|
||||
WEB_SERVER_PORT=1337
|
||||
|
||||
WELCOME_VERSION=1.0
|
||||
|
||||
# Custom Site info
|
||||
DOMAIN=lbry.tv
|
||||
URL=https://lbry.tv
|
||||
|
||||
# UI
|
||||
SITE_TITLE=lbry.tv
|
||||
SITE_NAME=local.lbry.tv
|
||||
SITE_DESCRIPTION=Meet LBRY, an open, free, and community-controlled content wonderland.
|
||||
LOGO_TITLE=local.lbry.tv
|
||||
|
||||
##### ODYSEE SETTINGS #######
|
||||
|
||||
MATOMO_URL=https://analytics.lbry.com/
|
||||
MATOMO_ID=4
|
||||
|
||||
# Base config
|
||||
WEBPACK_WEB_PORT=9090
|
||||
WEBPACK_ELECTRON_PORT=9091
|
||||
WEB_SERVER_PORT=1337
|
||||
|
||||
## APIS
|
||||
LBRY_API_URL=https://api.odysee.com
|
||||
#LBRY_WEB_API=https://api.na-backend.odysee.com
|
||||
#LBRY_WEB_STREAMING_API=https://cdn.lbryplayer.xyz
|
||||
# deprecated:
|
||||
#LBRY_WEB_BUFFER_API=https://collector-service.api.lbry.tv/api/v1/events/video
|
||||
#COMMENT_SERVER_API=https://comments.lbry.com/api/v2
|
||||
WELCOME_VERSION=1.0
|
||||
|
||||
# STRIPE
|
||||
STRIPE_PUBLIC_KEY='pk_live_e8M4dRNnCCbmpZzduEUZBgJO'
|
||||
|
||||
## UI
|
||||
|
||||
LOADING_BAR_COLOR=#e50054
|
||||
|
||||
# IMAGE ASSETS
|
||||
YRBL_HAPPY_IMG_URL=https://spee.ch/spaceman-happy:a.png
|
||||
YRBL_SAD_IMG_URL=https://spee.ch/spaceman-sad:d.png
|
||||
LOGIN_IMG_URL=https://spee.ch/login:b.png
|
||||
LOGO=https://spee.ch/odysee-logo-png:3.png
|
||||
LOGO_TEXT_LIGHT=https://spee.ch/odysee-white-png:f.png
|
||||
LOGO_TEXT_DARK=https://spee.ch/odysee-png:2.png
|
||||
AVATAR_DEFAULT=https://spee.ch/spaceman-png:2.png
|
||||
FAVICON=https://spee.ch/favicon-png:c.png
|
||||
|
||||
# LOCALE
|
||||
DEFAULT_LANGUAGE=en
|
||||
|
||||
## LINKED CONTENT WHITELIST
|
||||
KNOWN_APP_DOMAINS=open.lbry.com,lbry.tv,lbry.lat,odysee.com
|
||||
|
||||
## CUSTOM CONTENT
|
||||
# If the following is true, copy custom/homepage.example.js to custom/homepage.js and modify
|
||||
CUSTOM_HOMEPAGE=true
|
||||
|
||||
# Add channels to auto-follow on firstrun (space delimited)
|
||||
AUTO_FOLLOW_CHANNELS=lbry://@Odysee#80d2590ad04e36fb1d077a9b9e3a8bba76defdf8 lbry://@OdyseeHelp#b58dfaeab6c70754d792cdd9b56ff59b90aea334
|
||||
|
||||
## FEATURES AND LIMITS
|
||||
SIMPLE_SITE=true
|
||||
BRANDED_SITE=odysee
|
||||
# SIMPLE_SITE REPLACEMENTS
|
||||
ENABLE_MATURE=false
|
||||
ENABLE_UI_NOTIFICATIONS=true
|
||||
ENABLE_WILD_WEST=true
|
||||
SHOW_TAGS_INTRO=false
|
||||
|
||||
# CENTRALIZED FEATURES
|
||||
ENABLE_COMMENT_REACTIONS=true
|
||||
ENABLE_FILE_REACTIONS=true
|
||||
ENABLE_CREATOR_REACTIONS=true
|
||||
ENABLE_NO_SOURCE_CLAIMS=true
|
||||
ENABLE_PREROLL_ADS=false
|
||||
SHOW_ADS=true
|
||||
|
||||
CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS=4
|
||||
CHANNEL_STAKED_LEVEL_LIVESTREAM=3
|
||||
WEB_PUBLISH_SIZE_LIMIT_GB=4
|
||||
|
||||
#SEARCH TYPES - comma-delimited
|
||||
LIGHTHOUSE_DEFAULT_TYPES=audio,video
|
|
@ -1,8 +0,0 @@
|
|||
const { v4: uuid } = require('uuid');
|
||||
const jsBundleId = uuid();
|
||||
|
||||
function getJsBundleId() {
|
||||
return jsBundleId;
|
||||
}
|
||||
|
||||
module.exports = { getJsBundleId };
|
|
@ -1,10 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import fileViewerEmbeddedEnded from './view';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { makeSelectTagInClaimOrChannelForUri } from 'redux/selectors/claims';
|
||||
import { PREFERENCE_EMBED } from 'constants/tags';
|
||||
|
||||
export default connect((state, props) => ({
|
||||
isAuthenticated: selectUserVerifiedEmail(state),
|
||||
preferEmbed: makeSelectTagInClaimOrChannelForUri(props.uri, PREFERENCE_EMBED)(state),
|
||||
}))(fileViewerEmbeddedEnded);
|
|
@ -1,82 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { withRouter } from 'react-router';
|
||||
import { URL, SITE_NAME } from 'config';
|
||||
import Logo from 'component/logo';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
isAuthenticated: boolean,
|
||||
preferEmbed: boolean,
|
||||
};
|
||||
|
||||
function FileViewerEmbeddedEnded(props: Props) {
|
||||
const { uri, isAuthenticated, preferEmbed } = props;
|
||||
|
||||
const prompts = isAuthenticated
|
||||
? {
|
||||
discuss_auth: `Continue the discussion on ${SITE_NAME}`,
|
||||
tip_auth: 'Always tip your creators',
|
||||
}
|
||||
: {
|
||||
bigtech_unauth: 'Together, we can take back control from big tech',
|
||||
discuss_unauth: `Continue the discussion on ${SITE_NAME}`,
|
||||
find_unauth: `Find more great content on ${SITE_NAME}`,
|
||||
a_b_unauth: "We test a lot of messages here. Wouldn't it be funny if the one telling you that did the best?",
|
||||
earn_unauth: `Join ${SITE_NAME} and earn to watch.`,
|
||||
blockchain_unauth: "Now if anyone asks, you can say you've used a blockchain.",
|
||||
};
|
||||
|
||||
const promptKeys = Object.keys(prompts);
|
||||
const promptKey = promptKeys[Math.floor(Math.random() * promptKeys.length)];
|
||||
// $FlowFixMe
|
||||
const prompt = prompts[promptKey];
|
||||
const lbrytvLink = `${URL}${formatLbryUrlForWeb(uri)}?src=${promptKey}`;
|
||||
const showReplay = Boolean(window.player);
|
||||
|
||||
return (
|
||||
<div className="file-viewer__overlay">
|
||||
<div className="file-viewer__overlay-secondary">
|
||||
<Button className="file-viewer__overlay-logo" href={URL} disabled={preferEmbed}>
|
||||
<Logo type={'embed'} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="file-viewer__overlay-title file-viewer_embed-ended-title">
|
||||
<p>{prompt}</p>
|
||||
</div>
|
||||
<div className="file-viewer__overlay-actions">
|
||||
<>
|
||||
{showReplay && (
|
||||
<Button
|
||||
title={__('Replay')}
|
||||
button="link"
|
||||
label={preferEmbed ? __('Replay') : undefined}
|
||||
iconRight={ICONS.REPLAY}
|
||||
onClick={() => {
|
||||
if (window.player) window.player.play();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!preferEmbed && (
|
||||
<>
|
||||
<Button label={__('Discuss')} iconRight={ICONS.EXTERNAL} button="primary" href={lbrytvLink} />
|
||||
{!isAuthenticated && (
|
||||
<Button
|
||||
label={__('Join %SITE_NAME%', { SITE_NAME })}
|
||||
button="secondary"
|
||||
href={`${URL}/$/signup?src=embed_signup`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(FileViewerEmbeddedEnded);
|
|
@ -1,45 +0,0 @@
|
|||
import { SDK_API_PATH } from 'ui';
|
||||
import { useEffect } from 'react';
|
||||
import { getAuthToken } from 'util/saved-passwords';
|
||||
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
|
||||
|
||||
import fetchWithTimeout from 'util/fetch';
|
||||
|
||||
const STATUS_TIMEOUT_LIMIT = 10000;
|
||||
export const STATUS_OK = 'ok';
|
||||
export const STATUS_DEGRADED = 'degraded';
|
||||
export const STATUS_FAILING = 'failing';
|
||||
export const STATUS_DOWN = 'down';
|
||||
|
||||
const getParams = (user) => {
|
||||
const headers = {};
|
||||
const token = getAuthToken();
|
||||
if (token && user && user.has_verified_email) {
|
||||
headers[X_LBRY_AUTH_TOKEN] = token;
|
||||
}
|
||||
const params = { headers };
|
||||
return params;
|
||||
};
|
||||
|
||||
export function useDegradedPerformance(onDegradedPerformanceCallback, user) {
|
||||
const hasUser = user !== undefined && user !== null;
|
||||
|
||||
useEffect(() => {
|
||||
if (hasUser) {
|
||||
// The status endpoint is the only endpoint at "v2" currently
|
||||
// This should be moved into the config once more endpoints are using it
|
||||
const STATUS_ENDPOINT = `${SDK_API_PATH}/status`.replace('v1', 'v2');
|
||||
|
||||
fetchWithTimeout(STATUS_TIMEOUT_LIMIT, fetch(STATUS_ENDPOINT, getParams(user)))
|
||||
.then((response) => response.json())
|
||||
.then((status) => {
|
||||
if (status.general_state !== STATUS_OK) {
|
||||
onDegradedPerformanceCallback(STATUS_FAILING);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
onDegradedPerformanceCallback(STATUS_FAILING);
|
||||
});
|
||||
}
|
||||
}, [hasUser]);
|
||||
}
|
39
web/index.js
39
web/index.js
|
@ -1,39 +0,0 @@
|
|||
const config = require('../config');
|
||||
const path = require('path');
|
||||
const Koa = require('koa');
|
||||
const serve = require('koa-static');
|
||||
const logger = require('koa-logger');
|
||||
const router = require('./src/routes');
|
||||
const redirectMiddleware = require('./middleware/redirect');
|
||||
const cacheControlMiddleware = require('./middleware/cache-control');
|
||||
const iframeDestroyerMiddleware = require('./middleware/iframe-destroyer');
|
||||
|
||||
const app = new Koa();
|
||||
const DIST_ROOT = path.resolve(__dirname, 'dist');
|
||||
|
||||
app.proxy = true;
|
||||
|
||||
app.use(async (ctx, next) => {
|
||||
try {
|
||||
await next();
|
||||
} catch (err) {
|
||||
console.log('error: ', err);
|
||||
ctx.status = err.status || 500;
|
||||
ctx.body = err.message;
|
||||
}
|
||||
});
|
||||
|
||||
app.use(logger());
|
||||
app.use(cacheControlMiddleware);
|
||||
app.use(redirectMiddleware);
|
||||
app.use(iframeDestroyerMiddleware);
|
||||
|
||||
// Check if the request url matches any assets inside of /dist
|
||||
app.use(serve(DIST_ROOT, {
|
||||
maxage: 3600000, // set a cache time of one hour, helpful for mobile dev
|
||||
}));
|
||||
|
||||
app.use(serve(DIST_ROOT)); // Check if the request url matches any assets inside of /dist
|
||||
|
||||
app.use(router.routes());
|
||||
app.listen(config.WEB_SERVER_PORT, () => `Server up at localhost:${config.WEB_SERVER_PORT}`);
|
247
web/lbry.js
247
web/lbry.js
|
@ -1,247 +0,0 @@
|
|||
// Disabled flow in this copy. This copy is for uncompiled web server ES5 require()s.
|
||||
require('proxy-polyfill');
|
||||
|
||||
const CHECK_DAEMON_STARTED_TRY_NUMBER = 200;
|
||||
//
|
||||
// Basic LBRY sdk connection config
|
||||
// Offers a proxy to call LBRY sdk methods
|
||||
|
||||
//
|
||||
const Lbry = {
|
||||
isConnected: false,
|
||||
connectPromise: null,
|
||||
daemonConnectionString: 'http://localhost:5279',
|
||||
alternateConnectionString: '',
|
||||
methodsUsingAlternateConnectionString: [],
|
||||
apiRequestHeaders: { 'Content-Type': 'application/json-rpc' },
|
||||
|
||||
// Allow overriding daemon connection string (e.g. to `/api/proxy` for lbryweb)
|
||||
setDaemonConnectionString: (value) => {
|
||||
Lbry.daemonConnectionString = value;
|
||||
},
|
||||
|
||||
setApiHeader: (key, value) => {
|
||||
Lbry.apiRequestHeaders = Object.assign(Lbry.apiRequestHeaders, { [key]: value });
|
||||
},
|
||||
|
||||
unsetApiHeader: (key) => {
|
||||
Object.keys(Lbry.apiRequestHeaders).includes(key) && delete Lbry.apiRequestHeaders['key'];
|
||||
},
|
||||
// Allow overriding Lbry methods
|
||||
overrides: {},
|
||||
setOverride: (methodName, newMethod) => {
|
||||
Lbry.overrides[methodName] = newMethod;
|
||||
},
|
||||
getApiRequestHeaders: () => Lbry.apiRequestHeaders,
|
||||
|
||||
// Returns a human readable media type based on the content type or extension of a file that is returned by the sdk
|
||||
getMediaType: (contentType, fileName) => {
|
||||
if (fileName) {
|
||||
const formats = [
|
||||
[/\.(mp4|m4v|webm|flv|f4v|ogv)$/i, 'video'],
|
||||
[/\.(mp3|m4a|aac|wav|flac|ogg|opus)$/i, 'audio'],
|
||||
[/\.(jpeg|jpg|png|gif|svg|webp)$/i, 'image'],
|
||||
[/\.(h|go|ja|java|js|jsx|c|cpp|cs|css|rb|scss|sh|php|py)$/i, 'script'],
|
||||
[/\.(html|json|csv|txt|log|md|markdown|docx|pdf|xml|yml|yaml)$/i, 'document'],
|
||||
[/\.(pdf|odf|doc|docx|epub|org|rtf)$/i, 'e-book'],
|
||||
[/\.(stl|obj|fbx|gcode)$/i, '3D-file'],
|
||||
[/\.(cbr|cbt|cbz)$/i, 'comic-book'],
|
||||
[/\.(lbry)$/i, 'application'],
|
||||
];
|
||||
|
||||
const res = formats.reduce((ret, testpair) => {
|
||||
switch (testpair[0].test(ret)) {
|
||||
case true:
|
||||
return testpair[1];
|
||||
default:
|
||||
return ret;
|
||||
}
|
||||
}, fileName);
|
||||
return res === fileName ? 'unknown' : res;
|
||||
} else if (contentType) {
|
||||
// $FlowFixMe
|
||||
return /^[^/]+/.exec(contentType)[0];
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
},
|
||||
|
||||
//
|
||||
// Lbry SDK Methods
|
||||
// https://lbry.tech/api/sdk
|
||||
//
|
||||
status: (params = {}) => daemonCallWithResult('status', params),
|
||||
stop: () => daemonCallWithResult('stop', {}),
|
||||
version: () => daemonCallWithResult('version', {}),
|
||||
|
||||
// Claim fetching and manipulation
|
||||
resolve: (params) => daemonCallWithResult('resolve', params),
|
||||
get: (params) => daemonCallWithResult('get', params),
|
||||
claim_search: (params) => daemonCallWithResult('claim_search', params),
|
||||
claim_list: (params) => daemonCallWithResult('claim_list', params),
|
||||
channel_create: (params) => daemonCallWithResult('channel_create', params),
|
||||
channel_update: (params) => daemonCallWithResult('channel_update', params),
|
||||
channel_import: (params) => daemonCallWithResult('channel_import', params),
|
||||
channel_list: (params) => daemonCallWithResult('channel_list', params),
|
||||
stream_abandon: (params) => daemonCallWithResult('stream_abandon', params),
|
||||
stream_list: (params) => daemonCallWithResult('stream_list', params),
|
||||
channel_abandon: (params) => daemonCallWithResult('channel_abandon', params),
|
||||
channel_sign: (params) => daemonCallWithResult('channel_sign', params),
|
||||
support_create: (params) => daemonCallWithResult('support_create', params),
|
||||
support_list: (params) => daemonCallWithResult('support_list', params),
|
||||
stream_repost: (params) => daemonCallWithResult('stream_repost', params),
|
||||
collection_resolve: (params) => daemonCallWithResult('collection_resolve', params),
|
||||
collection_list: (params) => daemonCallWithResult('collection_list', params),
|
||||
collection_create: (params) => daemonCallWithResult('collection_create', params),
|
||||
collection_update: (params) => daemonCallWithResult('collection_update', params),
|
||||
|
||||
// File fetching and manipulation
|
||||
file_list: (params = {}) => daemonCallWithResult('file_list', params),
|
||||
file_delete: (params = {}) => daemonCallWithResult('file_delete', params),
|
||||
file_set_status: (params = {}) => daemonCallWithResult('file_set_status', params),
|
||||
blob_delete: (params = {}) => daemonCallWithResult('blob_delete', params),
|
||||
blob_list: (params = {}) => daemonCallWithResult('blob_list', params),
|
||||
|
||||
// Wallet utilities
|
||||
wallet_balance: (params = {}) => daemonCallWithResult('wallet_balance', params),
|
||||
wallet_decrypt: () => daemonCallWithResult('wallet_decrypt', {}),
|
||||
wallet_encrypt: (params = {}) => daemonCallWithResult('wallet_encrypt', params),
|
||||
wallet_unlock: (params = {}) => daemonCallWithResult('wallet_unlock', params),
|
||||
wallet_list: (params = {}) => daemonCallWithResult('wallet_list', params),
|
||||
wallet_send: (params = {}) => daemonCallWithResult('wallet_send', params),
|
||||
wallet_status: (params = {}) => daemonCallWithResult('wallet_status', params),
|
||||
address_is_mine: (params = {}) => daemonCallWithResult('address_is_mine', params),
|
||||
address_unused: (params = {}) => daemonCallWithResult('address_unused', params),
|
||||
address_list: (params = {}) => daemonCallWithResult('address_list', params),
|
||||
transaction_list: (params = {}) => daemonCallWithResult('transaction_list', params),
|
||||
utxo_release: (params = {}) => daemonCallWithResult('utxo_release', params),
|
||||
support_abandon: (params = {}) => daemonCallWithResult('support_abandon', params),
|
||||
purchase_list: (params = {}) => daemonCallWithResult('purchase_list', params),
|
||||
txo_list: (params = {}) => daemonCallWithResult('txo_list', params),
|
||||
|
||||
sync_hash: (params = {}) => daemonCallWithResult('sync_hash', params),
|
||||
sync_apply: (params = {}) => daemonCallWithResult('sync_apply', params),
|
||||
|
||||
// Preferences
|
||||
preference_get: (params = {}) => daemonCallWithResult('preference_get', params),
|
||||
preference_set: (params = {}) => daemonCallWithResult('preference_set', params),
|
||||
|
||||
// Comments
|
||||
comment_list: (params = {}) => daemonCallWithResult('comment_list', params),
|
||||
comment_create: (params = {}) => daemonCallWithResult('comment_create', params),
|
||||
comment_hide: (params = {}) => daemonCallWithResult('comment_hide', params),
|
||||
comment_abandon: (params = {}) => daemonCallWithResult('comment_abandon', params),
|
||||
comment_update: (params = {}) => daemonCallWithResult('comment_update', params),
|
||||
|
||||
// Connect to the sdk
|
||||
connect: () => {
|
||||
if (Lbry.connectPromise === null) {
|
||||
Lbry.connectPromise = new Promise((resolve, reject) => {
|
||||
let tryNum = 0;
|
||||
// Check every half second to see if the daemon is accepting connections
|
||||
function checkDaemonStarted() {
|
||||
tryNum += 1;
|
||||
Lbry.status()
|
||||
.then(resolve)
|
||||
.catch(() => {
|
||||
if (tryNum <= CHECK_DAEMON_STARTED_TRY_NUMBER) {
|
||||
setTimeout(checkDaemonStarted, tryNum < 50 ? 400 : 1000);
|
||||
} else {
|
||||
reject(new Error('Unable to connect to LBRY'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkDaemonStarted();
|
||||
});
|
||||
}
|
||||
|
||||
// Flow thinks this could be empty, but it will always reuturn a promise
|
||||
// $FlowFixMe
|
||||
return Lbry.connectPromise;
|
||||
},
|
||||
|
||||
publish: (params = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (Lbry.overrides.publish) {
|
||||
Lbry.overrides.publish(params).then(resolve, reject);
|
||||
} else {
|
||||
apiCall('publish', params, resolve, reject);
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
function checkAndParse(response) {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response.json();
|
||||
}
|
||||
return response.json().then((json) => {
|
||||
let error;
|
||||
if (json.error) {
|
||||
const errorMessage = typeof json.error === 'object' ? json.error.message : json.error;
|
||||
error = new Error(errorMessage);
|
||||
} else {
|
||||
error = new Error('Protocol error with unknown response signature');
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
function apiCall(method, params, resolve, reject) {
|
||||
const counter = new Date().getTime();
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: Lbry.apiRequestHeaders,
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
params,
|
||||
id: counter,
|
||||
}),
|
||||
};
|
||||
|
||||
const connectionString = Lbry.methodsUsingAlternateConnectionString.includes(method)
|
||||
? Lbry.alternateConnectionString
|
||||
: Lbry.daemonConnectionString;
|
||||
return fetch(connectionString + '?m=' + method, options)
|
||||
.then(checkAndParse)
|
||||
.then((response) => {
|
||||
const error = response.error || (response.result && response.result.error);
|
||||
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
return resolve(response.result);
|
||||
})
|
||||
.catch(reject);
|
||||
}
|
||||
|
||||
function daemonCallWithResult(name, params = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiCall(
|
||||
name,
|
||||
params,
|
||||
(result) => {
|
||||
resolve(result);
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// This is only for a fallback
|
||||
// If there is a Lbry method that is being called by an app, it should be added to /flow-typed/Lbry.js
|
||||
const lbryProxy = new Proxy(Lbry, {
|
||||
get(target, name) {
|
||||
if (name in target) {
|
||||
return target[name];
|
||||
}
|
||||
|
||||
return (params = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
apiCall(name, params, resolve, reject);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = { lbryProxy, apiCall };
|
|
@ -1,38 +0,0 @@
|
|||
const SIX_MONTHS_IN_SECONDS = 15552000;
|
||||
|
||||
const STATIC_ASSET_PATHS = [
|
||||
'/public/font/font-v1.css',
|
||||
'/public/font/v1/300.woff',
|
||||
'/public/font/v1/300i.woff',
|
||||
'/public/font/v1/400.woff',
|
||||
'/public/font/v1/400i.woff',
|
||||
'/public/font/v1/700.woff',
|
||||
'/public/font/v1/700i.woff',
|
||||
'/public/favicon.png', // LBRY icon
|
||||
'/public/favicon-spaceman.png',
|
||||
'/public/img/busy.gif',
|
||||
'/public/img/fileRenderPlaceholder.png',
|
||||
'/public/img/gerbil-happy.png',
|
||||
'/public/img/gerbil-sad.png',
|
||||
'/public/img/placeholder.png',
|
||||
'/public/img/placeholderTx.gif',
|
||||
'/public/img/thumbnail-broken.png',
|
||||
'/public/img/thumbnail-missing.png',
|
||||
'/public/img/total-background.png',
|
||||
];
|
||||
|
||||
async function redirectMiddleware(ctx, next) {
|
||||
const {
|
||||
request: { url },
|
||||
} = ctx;
|
||||
|
||||
const HASHED_JS_REGEX = /^\/public\/.*[a-fA-F0-9]{12}\.js$/i;
|
||||
|
||||
if (STATIC_ASSET_PATHS.includes(url) || HASHED_JS_REGEX.test(url)) {
|
||||
ctx.set('Cache-Control', `public, max-age=${SIX_MONTHS_IN_SECONDS}`);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
module.exports = redirectMiddleware;
|
|
@ -1,16 +0,0 @@
|
|||
const PAGES = require('../../ui/constants/pages');
|
||||
|
||||
async function iframeDestroyerMiddleware(ctx, next) {
|
||||
const {
|
||||
request: { path },
|
||||
} = ctx;
|
||||
const decodedPath = decodeURIComponent(path);
|
||||
|
||||
if (!decodedPath.startsWith(`/$/${PAGES.EMBED}`)) {
|
||||
ctx.set('X-Frame-Options', 'DENY');
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
module.exports = iframeDestroyerMiddleware;
|
|
@ -1,67 +0,0 @@
|
|||
const PAGES = require('../../ui/constants/pages');
|
||||
// const config = require('../../config');
|
||||
|
||||
function formatInAppUrl(path) {
|
||||
// Determine if we need to add a leading "/$/" for app pages
|
||||
const APP_PAGE_REGEX = /(\?)([a-z]*)(.*)/;
|
||||
const appPageMatches = APP_PAGE_REGEX.exec(path);
|
||||
|
||||
if (appPageMatches && appPageMatches.length) {
|
||||
// Definitely an app page (or it's formatted like one)
|
||||
const [, , page, queryString] = appPageMatches;
|
||||
|
||||
if (Object.values(PAGES).includes(page)) {
|
||||
let actualUrl = '/$/' + page;
|
||||
|
||||
if (queryString) {
|
||||
actualUrl += `?${queryString.slice(1)}`;
|
||||
}
|
||||
|
||||
return actualUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Regular claim url
|
||||
return path;
|
||||
}
|
||||
|
||||
async function redirectMiddleware(ctx, next) {
|
||||
const requestHost = ctx.host;
|
||||
const path = ctx.path;
|
||||
const url = ctx.url;
|
||||
|
||||
// Getting err: too many redirects on some urls because of this
|
||||
// Need a better solution
|
||||
// const decodedUrl = decodeURIComponent(url);
|
||||
|
||||
// if (decodedUrl !== url) {
|
||||
// ctx.redirect(decodedUrl);
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!path.startsWith('/$/') && path.match(/^([^@/:]+)\/([^:/]+)$/)) {
|
||||
ctx.redirect(url.replace(/^([^@/:]+)\/([^:/]+)(:(\/.*))/, '$1:$2')); // test against path, but use ctx.url to retain parameters
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestHost === 'open.lbry.com' || requestHost === 'open.lbry.io') {
|
||||
const openQuery = '?src=open';
|
||||
// let redirectUrl = config.URL + formatInAppUrl(url);
|
||||
// Blame tom for this, not me
|
||||
let redirectUrl = 'https://odysee.com' + formatInAppUrl(url);
|
||||
|
||||
if (redirectUrl.includes('?')) {
|
||||
redirectUrl = redirectUrl.replace('?', `${openQuery}&`);
|
||||
} else {
|
||||
redirectUrl += openQuery;
|
||||
}
|
||||
ctx.status = 301;
|
||||
ctx.redirect(redirectUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
// No redirects needed
|
||||
await next();
|
||||
}
|
||||
|
||||
module.exports = redirectMiddleware;
|
|
@ -1,59 +0,0 @@
|
|||
{
|
||||
"name": "lbry.tv",
|
||||
"version": "0.0.0",
|
||||
"description": "A web based browser for the LBRY network, a digital marketplace controlled by its users.",
|
||||
"keywords": [
|
||||
"lbry"
|
||||
],
|
||||
"license": "MIT",
|
||||
"homepage": "https://lbry.com/",
|
||||
"bugs": {
|
||||
"url": "https://github.com/lbryio/lbry-desktop/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lbryio/lbry-desktop"
|
||||
},
|
||||
"author": {
|
||||
"name": "LBRY Inc.",
|
||||
"email": "hello@lbry.com"
|
||||
},
|
||||
"main": "./index.js",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production webpack --progess --config webpack.config.js",
|
||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot --progress --config webpack.config.js",
|
||||
"dev:server": "nodemon index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koa/router": "^8.0.2",
|
||||
"cross-env": "^6.0.3",
|
||||
"koa": "^2.11.0",
|
||||
"koa-logger": "^3.2.1",
|
||||
"koa-send": "^5.0.0",
|
||||
"koa-static": "^5.0.0",
|
||||
"mysql": "^2.17.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
"uuid": "^8.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.3.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.6.2",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.2.3",
|
||||
"@babel/plugin-transform-runtime": "^7.4.3",
|
||||
"@babel/polyfill": "^7.2.5",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/register": "^7.0.0",
|
||||
"cache-loader": "^4.1.0",
|
||||
"nodemon": "^1.19.4",
|
||||
"speed-measure-webpack-plugin": "^1.3.1",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-bundle-analyzer": "^3.6.0",
|
||||
"webpack-dev-server": "^3.9.0",
|
||||
"webpack-merge": "^4.2.2",
|
||||
"write-file-webpack-plugin": "^4.5.1"
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
import PageCode2257 from './view';
|
||||
export default PageCode2257;
|
|
@ -1,47 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Page from 'component/page';
|
||||
import Card from 'component/common/card';
|
||||
|
||||
const Code2257Page = () => {
|
||||
return (
|
||||
<Page>
|
||||
<Card
|
||||
title="18 USC 2257 Statement: lbry.tv"
|
||||
body={
|
||||
<div>
|
||||
<p>
|
||||
lbry.tv is not a producer (primary or secondary) of any and all of the content found on the website
|
||||
(lbry.tv). With respect to the records as per 18 USC 2257 for any and all content found on this site,
|
||||
please kindly direct your request to the site for which the content was produced.
|
||||
</p>
|
||||
<p>
|
||||
lbry.tv is a video sharing site in which allows for the uploading, sharing and general viewing of various
|
||||
types of adult content and while lbry.tv does the best it can with verifying compliance, it may not be
|
||||
100% accurate.
|
||||
</p>
|
||||
<p>
|
||||
lbry.tv abides by the following procedures to ensure compliance:
|
||||
<ul>
|
||||
<li>Requiring all users to be 18 years of age to upload videos.</li>
|
||||
<li>
|
||||
When uploading, user must verify the content; assure he/she is 18 years of age; certify that he/she
|
||||
keeps records of the models in the content and that they are over 18 years of age.
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p>
|
||||
For further assistance and/or information in finding the content's originating site, please contact
|
||||
lbry.tv compliance at copyright@lbry.com
|
||||
</p>
|
||||
<p>
|
||||
Users of lbry.tv who come across such content are urged to flag it as inappropriate by clicking 'Report
|
||||
this video' link found below each video.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
export default Code2257Page;
|
|
@ -1,71 +0,0 @@
|
|||
@charset "utf-8";
|
||||
|
||||
@import '../../ui/scss/init/reset';
|
||||
@import '../../ui/scss/init/vars';
|
||||
@import '../../ui/scss/init/mixins';
|
||||
@import '../../ui/scss/init/gui';
|
||||
@import '../../ui/scss/init/base-theme';
|
||||
|
||||
@import 'themes/lbrytv/light.scss';
|
||||
@import 'themes/lbrytv/dark.scss';
|
||||
@import '../../ui/scss/component/ads';
|
||||
@import '../../ui/scss/component/animation';
|
||||
@import '../../ui/scss/component/badge';
|
||||
@import '../../ui/scss/component/block-list';
|
||||
@import '../../ui/scss/component/button';
|
||||
@import '../../ui/scss/component/card';
|
||||
@import '../../ui/scss/component/channel';
|
||||
@import '../../ui/scss/component/channel-mention';
|
||||
@import '../../ui/scss/component/claim-list';
|
||||
@import '../../ui/scss/component/collection';
|
||||
@import '../../ui/scss/component/comments';
|
||||
@import '../../ui/scss/component/content';
|
||||
@import '../../ui/scss/component/dat-gui';
|
||||
@import '../../ui/scss/component/embed-player';
|
||||
@import '../../ui/scss/component/expandable';
|
||||
@import '../../ui/scss/component/expanding-details';
|
||||
@import '../../ui/scss/component/file-drop';
|
||||
@import '../../ui/scss/component/file-list';
|
||||
@import '../../ui/scss/component/file-properties';
|
||||
@import '../../ui/scss/component/file-render';
|
||||
@import '../../ui/scss/component/footer';
|
||||
@import '../../ui/scss/component/form-field';
|
||||
@import '../../ui/scss/component/header';
|
||||
@import '../../ui/scss/component/icon';
|
||||
@import '../../ui/scss/component/main';
|
||||
@import '../../ui/scss/component/markdown-editor';
|
||||
@import '../../ui/scss/component/markdown-preview';
|
||||
@import '../../ui/scss/component/media';
|
||||
@import '../../ui/scss/component/menu-button';
|
||||
@import '../../ui/scss/component/modal';
|
||||
@import '../../ui/scss/component/nag';
|
||||
@import '../../ui/scss/component/navigation';
|
||||
@import '../../ui/scss/component/notification';
|
||||
@import '../../ui/scss/component/nudge';
|
||||
@import '../../ui/scss/component/pagination';
|
||||
@import '../../ui/scss/component/post';
|
||||
@import '../../ui/scss/component/purchase';
|
||||
@import '../../ui/scss/component/placeholder';
|
||||
@import '../../ui/scss/component/progress';
|
||||
@import '../../ui/scss/component/search';
|
||||
@import '../../ui/scss/component/claim-search';
|
||||
@import '../../ui/scss/component/section';
|
||||
@import '../../ui/scss/component/share';
|
||||
@import '../../ui/scss/component/snack-bar';
|
||||
@import '../../ui/scss/component/spinner';
|
||||
@import '../../ui/scss/component/splash';
|
||||
@import '../../ui/scss/component/status-bar';
|
||||
@import '../../ui/scss/component/superchat';
|
||||
@import '../../ui/scss/component/syntax-highlighter';
|
||||
@import '../../ui/scss/component/table';
|
||||
@import '../../ui/scss/component/livestream';
|
||||
@import '../../ui/scss/component/tabs';
|
||||
@import '../../ui/scss/component/tooltip';
|
||||
@import '../../ui/scss/component/txo-list';
|
||||
@import '../../ui/scss/component/videojs';
|
||||
@import '../../ui/scss/component/tags';
|
||||
@import '../../ui/scss/component/wunderbar';
|
||||
@import '../../ui/scss/component/yrbl';
|
||||
@import '../../ui/scss/component/empty';
|
||||
@import '../../ui/scss/component/stripe-card';
|
||||
@import '../../ui/scss/component/wallet-tip-send';
|
|
@ -1,71 +0,0 @@
|
|||
@charset "utf-8";
|
||||
|
||||
@import 'themes/odysee/init/reset';
|
||||
@import 'themes/odysee/init/vars';
|
||||
@import 'themes/odysee/init/mixins';
|
||||
@import 'themes/odysee/init/gui';
|
||||
@import 'themes/odysee/init/base-theme';
|
||||
|
||||
@import 'themes/odysee/light.scss';
|
||||
@import 'themes/odysee/dark.scss';
|
||||
@import '../../ui/scss/component/ads';
|
||||
@import '../../ui/scss/component/animation';
|
||||
@import '../../ui/scss/component/badge';
|
||||
@import '../../ui/scss/component/block-list';
|
||||
@import '../../ui/scss/component/button';
|
||||
@import '../../ui/scss/component/card';
|
||||
@import '../../ui/scss/component/channel';
|
||||
@import '../../ui/scss/component/channel-mention';
|
||||
@import '../../ui/scss/component/claim-list';
|
||||
@import '../../ui/scss/component/collection';
|
||||
@import '../../ui/scss/component/comments';
|
||||
@import '../../ui/scss/component/content';
|
||||
@import '../../ui/scss/component/dat-gui';
|
||||
@import '../../ui/scss/component/embed-player';
|
||||
@import '../../ui/scss/component/expandable';
|
||||
@import '../../ui/scss/component/expanding-details';
|
||||
@import '../../ui/scss/component/file-drop';
|
||||
@import '../../ui/scss/component/file-list';
|
||||
@import '../../ui/scss/component/file-properties';
|
||||
@import '../../ui/scss/component/file-render';
|
||||
@import '../../ui/scss/component/footer';
|
||||
@import '../../ui/scss/component/form-field';
|
||||
@import '../../ui/scss/component/header';
|
||||
@import '../../ui/scss/component/icon';
|
||||
@import '../../ui/scss/component/main';
|
||||
@import '../../ui/scss/component/markdown-editor';
|
||||
@import '../../ui/scss/component/markdown-preview';
|
||||
@import '../../ui/scss/component/media';
|
||||
@import '../../ui/scss/component/menu-button';
|
||||
@import '../../ui/scss/component/modal';
|
||||
@import '../../ui/scss/component/nag';
|
||||
@import '../../ui/scss/component/navigation';
|
||||
@import '../../ui/scss/component/notification';
|
||||
@import '../../ui/scss/component/nudge';
|
||||
@import '../../ui/scss/component/pagination';
|
||||
@import '../../ui/scss/component/post';
|
||||
@import '../../ui/scss/component/purchase';
|
||||
@import '../../ui/scss/component/placeholder';
|
||||
@import '../../ui/scss/component/progress';
|
||||
@import '../../ui/scss/component/search';
|
||||
@import '../../ui/scss/component/claim-search';
|
||||
@import '../../ui/scss/component/section';
|
||||
@import '../../ui/scss/component/share';
|
||||
@import '../../ui/scss/component/snack-bar';
|
||||
@import '../../ui/scss/component/spinner';
|
||||
@import '../../ui/scss/component/splash';
|
||||
@import '../../ui/scss/component/status-bar';
|
||||
@import '../../ui/scss/component/superchat';
|
||||
@import '../../ui/scss/component/syntax-highlighter';
|
||||
@import '../../ui/scss/component/table';
|
||||
@import '../../ui/scss/component/livestream';
|
||||
@import '../../ui/scss/component/tabs';
|
||||
@import '../../ui/scss/component/tooltip';
|
||||
@import '../../ui/scss/component/txo-list';
|
||||
@import '../../ui/scss/component/videojs';
|
||||
@import '../../ui/scss/component/tags';
|
||||
@import '../../ui/scss/component/wunderbar';
|
||||
@import '../../ui/scss/component/yrbl';
|
||||
@import '../../ui/scss/component/empty';
|
||||
@import '../../ui/scss/component/stripe-card';
|
||||
@import '../../ui/scss/component/wallet-tip-send';
|
|
@ -1,160 +0,0 @@
|
|||
[theme='dark'] {
|
||||
// Color overrides
|
||||
--color-primary: #2bbb90;
|
||||
--color-primary-alt: #3e675d;
|
||||
--color-primary-alt-2: #065f46;
|
||||
--color-primary-alt-3: #34e5b0;
|
||||
--color-secondary: #204166;
|
||||
--color-secondary-alt: #dbeafe;
|
||||
--color-secondary-alt-2: #bfdbfe;
|
||||
--color-secondary-alt-3: #2c5c8c;
|
||||
|
||||
// Structure
|
||||
--color-background: var(--color-gray-9);
|
||||
--color-background-overlay: #21252999;
|
||||
--color-border: #333338;
|
||||
--color-card-background: var(--color-gray-8);
|
||||
--color-card-background-highlighted: var(--color-gray-7);
|
||||
|
||||
// Text
|
||||
--color-text: var(--color-white);
|
||||
--color-text-subtitle: var(--color-gray-4);
|
||||
--color-text-empty: var(--color-text-subtitle);
|
||||
--color-text-help: #bbbbbb;
|
||||
--color-text-warning: #212529;
|
||||
--color-text-warning--background: var(--lbry-yellow-1);
|
||||
--color-text-error: #f87171;
|
||||
--color-error: #61373f;
|
||||
|
||||
// Tags (words)
|
||||
--color-tag-words: var(--color-text);
|
||||
--color-tag-words-bg: var(--color-gray-5);
|
||||
--color-tag-words-hover: var(--color-white);
|
||||
--color-tag-words-bg-hover: var(--color-gray-4);
|
||||
|
||||
// Header
|
||||
--color-header-background: var(--color-gray-8);
|
||||
--color-header-button: var(--color-gray-6);
|
||||
--color-header-button-hover: var(--color-gray-6);
|
||||
--color-header-button-active: var(--color-gray-6);
|
||||
|
||||
// Button
|
||||
--color-button-primary-bg: var(--color-primary-alt);
|
||||
--color-button-primary-bg-hover: var(--color-primary-alt-2);
|
||||
--color-button-primary-text: var(--color-gray-2);
|
||||
--color-button-primary-hover-text: var(--color-primary-alt);
|
||||
--color-button-secondary-bg: var(--color-secondary);
|
||||
--color-button-secondary-border: var(--color-secondary);
|
||||
--color-button-secondary-bg-hover: var(--color-secondary-alt-3);
|
||||
--color-button-secondary-text: var(--color-gray-2);
|
||||
--color-button-alt-bg: var(--color-gray-7);
|
||||
--color-button-alt-bg-hover: var(--color-gray-6);
|
||||
--color-button-alt-text: var(--color-gray-1);
|
||||
--color-button-border: var(--color-gray-5);
|
||||
--color-button-toggle-text: var(--color-gray-1);
|
||||
--color-link: var(--color-primary-alt-3);
|
||||
--color-link-hover: var(--color-text);
|
||||
--color-link-focus-bg: var(--color-gray-7);
|
||||
|
||||
// Input
|
||||
--color-input: var(--color-white);
|
||||
--color-input-label: var(--color-gray-3);
|
||||
--color-input-placeholder: var(--color-gray-1);
|
||||
--color-input-bg: var(--color-header-button);
|
||||
--color-input-bg-copyable: var(--color-gray-6);
|
||||
--color-input-border: var(--color-border);
|
||||
--color-input-border-active: var(--color-secondary);
|
||||
--color-input-toggle: var(--color-primary-alt-3);
|
||||
--color-input-toggle-bg: var(--color-input-bg);
|
||||
--color-input-toggle-bg-hover: var(--color-secondary);
|
||||
--color-input-bg-selected: var(--color-primary-alt);
|
||||
--color-input-prefix-bg: var(--color-gray-5);
|
||||
--color-input-prefix-border: var(--color-gray-4);
|
||||
--select-toggle-background: url("data:image/svg+xml,%3Csvg viewBox='0 0 96 96' xmlns='http://www.w3.org/2000/svg' fill='%23ffffff'%3E%3Cpath d='M17.172, 31.172c1.562, -1.562 4.095, -1.562 5.656, 0l25.172, 25.171l25.172, -25.171c1.562, -1.562 4.095, -1.562 5.656, 0c1.562, 1.562 1.562, 4.095 0, 5.656l-28, 28c-1.562, 1.562 -4.095, 1.562 -5.656, 0l-28, -28c-0.781, -0.781 -1.172, -1.805 -1.172, -2.828c0, -1.023 0.391, -2.047 1.172, -2.828Z'/%3E%3C/svg%3E%0A");
|
||||
|
||||
// Navigation
|
||||
--color-navigation-icon: var(--color-gray-4);
|
||||
--color-navigation-link: var(--color-gray-4);
|
||||
--color-navigation-active: var(--color-gray-7);
|
||||
--color-navigation-active-text: var(--color-gray-3);
|
||||
--color-navigation-hover: var(--color-gray-6);
|
||||
--color-navigation-hover-text: var(--color-gray-3);
|
||||
|
||||
// Tags
|
||||
--color-tag: var(--color-primary-alt-3);
|
||||
--color-tag-bg: var(--color-gray-7);
|
||||
--color-tag-hover: var(--color-white);
|
||||
--color-tag-bg-hover: var(--color-primary-alt);
|
||||
|
||||
// Menu
|
||||
--color-menu-background: var(--color-header-background);
|
||||
--color-menu-background--active: var(--color-gray-7);
|
||||
--color-menu-icon: var(--color-gray-4);
|
||||
--color-menu-icon-active: var(--color-gray-2);
|
||||
|
||||
// Comments
|
||||
--color-comment-menu: var(--color-gray-5);
|
||||
--color-comment-menu-hovering: var(--color-gray-2);
|
||||
--color-comment-threadline: #434b54;
|
||||
--color-comment-threadline-hover: var(--color-gray-4);
|
||||
--color-comment-highlighted: #484734;
|
||||
|
||||
// Snack
|
||||
--color-snack-bg: var(--color-secondary);
|
||||
|
||||
// Superchat
|
||||
--color-superchat-text: var(--color-black);
|
||||
--color-superchat-text__light: var(--color-text);
|
||||
--color-superchat: #fcd34d;
|
||||
--color-superchat__light: #ef4e1647;
|
||||
--color-superchat-2: #fde68a;
|
||||
--color-superchat-3: #fef3c7;
|
||||
--color-superchat-3__light: #58066087;
|
||||
--color-superchat-4: #fffbeb;
|
||||
|
||||
// Other
|
||||
--color-focus: #93c5fd50;
|
||||
--color-nag: var(--color-orange);
|
||||
--color-tab-text: var(--color-white);
|
||||
--color-tabs-background: var(--color-card-background);
|
||||
--color-tab-divider: var(--color-white);
|
||||
--color-modal-background: var(--color-card-background);
|
||||
--color-notice: #58563b;
|
||||
--color-purchased: #ffd580;
|
||||
--color-purchased-alt: var(--color-purchased);
|
||||
--color-purchased-text: var(--color-gray-5);
|
||||
--color-thumbnail-background: var(--color-gray-5);
|
||||
--color-tooltip-bg: #2f3337;
|
||||
--color-help-warning-bg: #d97706;
|
||||
--color-help-warning-text: white;
|
||||
--color-blockquote: var(--color-gray-5);
|
||||
--color-placeholder-background: #4e5862;
|
||||
--color-spinner-light: #5a6570;
|
||||
--color-spinner-dark: #212529;
|
||||
--color-login-graphic-background: var(--color-background);
|
||||
|
||||
// Editor
|
||||
--color-editor-cursor: var(--color-text);
|
||||
--color-editor-quote: #d3d3d3;
|
||||
--color-editor-tag: #efbe5d;
|
||||
--color-editor-attr: #68ccf9;
|
||||
--color-editor-string: #ff8b6b;
|
||||
--color-editor-inline-code-fg: #ce9178;
|
||||
--color-editor-inline-code-fg-preview: #e8b692;
|
||||
--color-editor-inline-code-bg: rgba(20, 68, 102, 0.3);
|
||||
--color-editor-inline-code-bg-preview: #464b50;
|
||||
--color-editor-selected: #264f78;
|
||||
--color-editor-link: var(--color-link);
|
||||
--color-editor-url: var(--color-editor-string);
|
||||
--color-editor-hr: var(--color-editor-tag);
|
||||
--color-editor-hr-preview: #a0a0a0;
|
||||
|
||||
// Ads
|
||||
--color-ads-background: #475057;
|
||||
--color-ads-text: #111;
|
||||
--color-ads-link: var(--color-primary-alt);
|
||||
|
||||
// Scrollbar
|
||||
--color-scrollbar-thumb-bg: rgba(255, 255, 255, 0.2);
|
||||
--color-scrollbar-track-bg: transparent;
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
:root {
|
||||
}
|
|
@ -1,757 +0,0 @@
|
|||
.file-page {
|
||||
.file-page__video-container + .card,
|
||||
.file-render + .card,
|
||||
.content__cover + .card,
|
||||
.card + .file-render,
|
||||
.card + .file-page__video-container,
|
||||
.card + .content__cover {
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
|
||||
.card + .file-render {
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
|
||||
.file-page__md {
|
||||
.file-viewer--document {
|
||||
margin-top: var(--spacing-l);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline;
|
||||
|
||||
.button__content {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.claim-link {
|
||||
.button {
|
||||
display: block;
|
||||
|
||||
.button__content {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media__actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-page__secondary-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
> * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.file-render {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
max-height: var(--inline-player-max-height);
|
||||
}
|
||||
|
||||
.file-render--video {
|
||||
background-color: black;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: black;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
animation: fadeInFromBlack 2s ease;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInFromBlack {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.file-render--embed {
|
||||
border-radius: 0;
|
||||
position: fixed;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.file-render--img-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.file-render--post-container {
|
||||
min-height: 30vh;
|
||||
}
|
||||
|
||||
.file-render__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.file-viewer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
iframe,
|
||||
webview,
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
max-height: var(--inline-player-max-height);
|
||||
}
|
||||
video {
|
||||
cursor: pointer;
|
||||
}
|
||||
.video-js.vjs-user-inactive.vjs-playing {
|
||||
video {
|
||||
cursor: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-render__viewer--comic {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.comic-viewer {
|
||||
width: 100%;
|
||||
height: calc(100vh - var(--header-height) - var(--spacing-m) * 2);
|
||||
max-height: var(--inline-player-max-height);
|
||||
}
|
||||
}
|
||||
|
||||
.file-viewer--iframe {
|
||||
display: flex; /*this eliminates extra height from whitespace, if someone edits this with a better technique, tell Jeremy*/
|
||||
/*
|
||||
ideally iframes would dynamiclly grow, see <IframeReact> for a start at this
|
||||
for now, since we don't know size, let's make as large as we can without being larger than available area
|
||||
*/
|
||||
iframe {
|
||||
height: calc(100vh - var(--header-height) - var(--spacing-m) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
.file-render__viewer--three {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.three-viewer {
|
||||
height: calc(100vh - var(--header-height) - var(--spacing-m) * 2);
|
||||
max-height: var(--inline-player-max-height);
|
||||
}
|
||||
}
|
||||
|
||||
.file-viewer__overlay {
|
||||
position: absolute;
|
||||
left: auto;
|
||||
right: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
color: var(--color-white);
|
||||
font-size: var(--font-body); /* put back font size from videojs change*/
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-l);
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
font-size: var(--font-small);
|
||||
}
|
||||
|
||||
.button--uri-indicator,
|
||||
.button--link {
|
||||
color: var(--color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.content__viewer--floating {
|
||||
.file-viewer__overlay-title,
|
||||
.file-viewer__overlay-secondary {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@media (max-width: $breakpoint-small) {
|
||||
.file-viewer__overlay-title,
|
||||
.file-viewer__overlay-secondary {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.file-viewer__overlay-title {
|
||||
text-align: center;
|
||||
font-size: var(--font-large);
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
.file-viewer__overlay-secondary {
|
||||
color: var(--color-text-subtitle);
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
.file-viewer__overlay-actions {
|
||||
.button + .button {
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
.file-viewer__overlay-logo {
|
||||
height: 2rem;
|
||||
max-height: 2rem; //embed logo height?
|
||||
width: 12rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
filter: drop-shadow(1px 2px 10px var(--color-gray-3));
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
margin-right: var(--spacing-m);
|
||||
width: 2.5rem;
|
||||
|
||||
.button__label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-viewer__overlay-logo--videoend {
|
||||
height: 3.5rem;
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.file-viewer--is-playing:not(:hover) .file-viewer__embedded-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-viewer__embedded-header {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
z-index: 2;
|
||||
height: 4rem;
|
||||
padding-left: var(--spacing-m);
|
||||
padding-right: var(--spacing-s);
|
||||
font-size: var(--font-large);
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
|
||||
.button {
|
||||
color: var(--color-white);
|
||||
z-index: 2;
|
||||
|
||||
.button__label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.credit-amount,
|
||||
.icon--Key {
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
padding: 0 var(--spacing-l);
|
||||
}
|
||||
}
|
||||
|
||||
.file-viewer__embedded-gradient {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
background: linear-gradient(#000000, #00000000 70%);
|
||||
height: 75px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.file-viewer__embedded-title {
|
||||
color: white;
|
||||
max-width: 75%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.file-viewer__embedded-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-small);
|
||||
margin-left: var(--spacing-m);
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
.file-render__content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
//
|
||||
// Custom viewers live below here
|
||||
// These either have custom class names that can't be changed or have styles that need to be overridden
|
||||
//
|
||||
|
||||
// Code-viewer
|
||||
.CodeMirror {
|
||||
@extend .file-render__content;
|
||||
|
||||
.cm-invalidchar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.CodeMirror .CodeMirror-lines {
|
||||
// is there really a .CodeMirror inside a .CodeMirror?
|
||||
padding: var(--spacing-s) 0;
|
||||
}
|
||||
|
||||
.CodeMirror-code {
|
||||
@include font-sans;
|
||||
letter-spacing: 0.1rem;
|
||||
}
|
||||
|
||||
.CodeMirror-gutters {
|
||||
background-color: var(--color-gray-1);
|
||||
border-right: 1px solid var(--color-gray-4);
|
||||
padding-right: var(--spacing-m);
|
||||
}
|
||||
|
||||
.CodeMirror-line {
|
||||
padding-left: var(--spacing-m);
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
color: var(--color-gray-5);
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Video
|
||||
// ****************************************************************************
|
||||
|
||||
.video-js-parent {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// By default no video js play button
|
||||
.vjs-big-play-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.video-js {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.vjs-modal-dialog .vjs-modal-dialog-content {
|
||||
position: relative;
|
||||
padding-top: 5rem;
|
||||
// Make sure no videojs message interferes with overlaying buttons
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vjs-control-bar {
|
||||
// background-color: rgba(0, 0, 0, 0.8);
|
||||
|
||||
.vjs-remaining-time {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vjs-current-time,
|
||||
.vjs-time-divider,
|
||||
.vjs-duration {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-picture-in-picture-control {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Video::Overlays
|
||||
// ****************************************************************************
|
||||
|
||||
.video-js {
|
||||
.vjs-overlay-playrate,
|
||||
.vjs-overlay-seeked {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
font-size: var(--font-large);
|
||||
width: auto;
|
||||
padding: 10px 30px;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
animation: fadeOutAnimation ease-in 0.6s;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeOutAnimation {
|
||||
0% {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Video - Mobile UI
|
||||
// ****************************************************************************
|
||||
|
||||
.video-js.vjs-mobile-ui {
|
||||
.vjs-control-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.vjs-touch-overlay:not(.show-play-toggle) {
|
||||
.vjs-control-bar {
|
||||
// Sync the controlBar's visibility with the overlay's
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-touch-overlay {
|
||||
&.show-play-toggle,
|
||||
&.skip {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
// Override the original's 'display: block' to avoid the big play button
|
||||
// from being squished to the side:
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
video::-internal-media-controls-overlay-cast-button {
|
||||
// Push the cast button above vjs-touch-overlay:
|
||||
z-index: 3;
|
||||
|
||||
// Move it to the upper-right since it will clash with "tap to unmute":
|
||||
left: unset;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.video-js.video-js.vjs-user-inactive {
|
||||
video::-internal-media-controls-overlay-cast-button {
|
||||
// (1) Android-Chrome's original Cast button behavior:
|
||||
// - If video is playing, fade out the button.
|
||||
// - If video is paused and video is tapped, display the button and stay on.
|
||||
// (2) We then injected another behavior:
|
||||
// - Display the button when '.vjs-touch-overlay' is displayed. However,
|
||||
// the 'controlslist' attribute hack that was used to do this results in the
|
||||
// button staying displayed without a fade-out timer.
|
||||
// (3) Ideally, we should sync the '.vjs-touch-overlay' visibility with the
|
||||
// cast button, similar to how to controlBar's visibility is synced above.
|
||||
// But I have no idea how to grab the sibling '.show-play-toggle' into the
|
||||
// css logic.
|
||||
// (4) So, this is the best that I can come up with: Whenever user is idle,
|
||||
// the button goes away. The only downside I know is the scenario of
|
||||
// "overlay is up and video is paused, but button goes away due to idle".
|
||||
// The user just needs to re-tap any empty space on the overlay to get the
|
||||
// Cast button again.
|
||||
opacity: 0;
|
||||
transition: opacity 1s ease;
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Layout and control visibility
|
||||
// ****************************************************************************
|
||||
|
||||
.video-js.vjs-fullscreen,
|
||||
.video-js:not(.vjs-fullscreen) {
|
||||
// --- Unhide desired components per layout ---
|
||||
&.vjs-layout-x-small {
|
||||
.vjs-playback-rate {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.vjs-layout-small {
|
||||
.vjs-current-time,
|
||||
.vjs-time-divider,
|
||||
.vjs-duration,
|
||||
.vjs-playback-rate {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Adjust spacing ---
|
||||
.vjs-current-time {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.vjs-duration {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.vjs-playback-rate .vjs-playback-rate-value {
|
||||
// Reduce the gigantic font a bit. Default was 1.5em.
|
||||
font-size: 1.25em;
|
||||
line-height: 2.5;
|
||||
}
|
||||
|
||||
.vjs-playback-rate .vjs-menu {
|
||||
// Extend the width to prevent a potential scrollbar from blocking the text.
|
||||
width: 8em;
|
||||
left: -2em;
|
||||
}
|
||||
}
|
||||
|
||||
.video-js.vjs-fullscreen {
|
||||
.vjs-button--theater-mode {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Tap-to-unmute
|
||||
// ****************************************************************************
|
||||
|
||||
.video-js--tap-to-unmute {
|
||||
visibility: hidden; // Start off as hidden.
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
top: var(--spacing-xs);
|
||||
left: var(--spacing-xs);
|
||||
padding: var(--spacing-xs) var(--spacing-m); // Make it comfy for touch.
|
||||
color: var(--color-gray-1);
|
||||
background: black;
|
||||
border: 1px solid var(--color-gray-4);
|
||||
opacity: 0.9;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--color-white);
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// ****************************************************************************
|
||||
|
||||
.video-js:hover {
|
||||
.vjs-big-play-button {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.file-render {
|
||||
.video-js {
|
||||
/*display: flex;*/
|
||||
/*align-items: center;*/
|
||||
/*justify-content: center;*/
|
||||
}
|
||||
|
||||
.vjs-big-play-button {
|
||||
@extend .button--icon;
|
||||
@extend .button--play;
|
||||
border: none;
|
||||
/*position: static;*/
|
||||
z-index: 2;
|
||||
|
||||
.vjs-icon-placeholder {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-menu-item-text,
|
||||
.vjs-icon-placeholder {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// ****************************************************************************
|
||||
|
||||
.file-render--embed {
|
||||
// on embeds, do not inject our colors until interaction
|
||||
.video-js:hover {
|
||||
.vjs-big-play-button {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-paused {
|
||||
.vjs-big-play-button {
|
||||
display: block;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-ended {
|
||||
.vjs-big-play-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.video-js--tap-to-unmute {
|
||||
margin-top: var(--spacing-xl);
|
||||
margin-left: var(--spacing-s);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-left: calc(var(--spacing-m) + var(--spacing-s));
|
||||
}
|
||||
}
|
||||
|
||||
.file-viewer {
|
||||
iframe,
|
||||
webview,
|
||||
img {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-viewer--ended-embed .vjs-big-play-button {
|
||||
display: none !important; // yes this is dumb, but this was broken and the above CSS was overriding
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Autoplay Countdown
|
||||
// ****************************************************************************
|
||||
|
||||
.autoplay-countdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.autoplay-countdown__timer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: var(--font-small);
|
||||
}
|
||||
.autoplay-countdown__counter {
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
|
||||
.autoplay-countdown__button {
|
||||
/* Border size and color */
|
||||
border: 3px solid transparent;
|
||||
/* Creates a circle */
|
||||
border-radius: 50%;
|
||||
/* Circle size */
|
||||
display: inline-block;
|
||||
height: 86px;
|
||||
width: 86px;
|
||||
/* Use transform to rotate to adjust where opening appears */
|
||||
transition: border 1s;
|
||||
transform: rotate(45deg);
|
||||
.button {
|
||||
background-color: transparent;
|
||||
transform: rotate(-45deg);
|
||||
&:hover {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.autoplay-countdown__button--4 {
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.autoplay-countdown__button--3 {
|
||||
border-top-color: #fff;
|
||||
border-right-color: #fff;
|
||||
}
|
||||
.autoplay-countdown__button--2 {
|
||||
border-top-color: #fff;
|
||||
border-right-color: #fff;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.autoplay-countdown__button--1 {
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// ****************************************************************************
|
||||
|
||||
.file__viewdate {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
margin-bottom: var(--spacing-s);
|
||||
|
||||
> :first-child {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
flex-direction: row;
|
||||
justify-content: start;
|
||||
|
||||
> :first-child {
|
||||
margin-bottom: 0;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-page__image {
|
||||
img {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
|
@ -1,713 +0,0 @@
|
|||
@import '../init/mixins';
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
.date-picker-input {
|
||||
height: var(--height-input);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid;
|
||||
color: var(--color-input);
|
||||
border-color: var(--color-input-border);
|
||||
background-color: var(--color-input-bg);
|
||||
padding-right: var(--spacing-s);
|
||||
padding-left: var(--spacing-s);
|
||||
|
||||
&:focus {
|
||||
@include focus;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-input-placeholder);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
|
||||
& + label {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&[type='range'] {
|
||||
height: auto;
|
||||
height: 0.5rem;
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
checkbox-element,
|
||||
radio-element,
|
||||
select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
background-image: var(--select-toggle-background);
|
||||
background-position: 99% center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1rem;
|
||||
padding-right: var(--spacing-l);
|
||||
padding-left: var(--spacing-s);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
fieldset-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
&.fieldset-group--smushed {
|
||||
fieldset-section + fieldset-section {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldset-section,
|
||||
fieldset-group,
|
||||
form,
|
||||
.checkbox,
|
||||
.radio,
|
||||
.form-field--SimpleMDE,
|
||||
.form-field__help {
|
||||
+ fieldset-section,
|
||||
+ fieldset-group,
|
||||
+ form,
|
||||
+ .checkbox,
|
||||
+ .radio,
|
||||
+ .form-field--SimpleMDE {
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
|
||||
+ .form-field__help {
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
fieldset-section,
|
||||
.checkbox,
|
||||
.radio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: var(--font-small);
|
||||
color: var(--color-input-label);
|
||||
display: inline-block;
|
||||
margin-bottom: 0.1rem;
|
||||
|
||||
.icon__lbc {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
input-submit {
|
||||
display: flex;
|
||||
|
||||
& > *:first-child,
|
||||
& > *:nth-child(2) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& > *:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
& > *:nth-child(2) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox,
|
||||
.radio {
|
||||
position: relative;
|
||||
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
height: var(--height-checkbox);
|
||||
width: var(--height-checkbox);
|
||||
position: absolute;
|
||||
border: none;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
|
||||
&:disabled + label {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
font-size: var(--font-base);
|
||||
padding-left: calc(var(--height-checkbox) + var(--spacing-s));
|
||||
min-height: var(--height-checkbox);
|
||||
|
||||
&::before {
|
||||
background-color: var(--color-input-toggle-bg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
background-color: var(--color-input-toggle-bg-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label::before,
|
||||
label::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
}
|
||||
|
||||
// Hide the checkmark by default
|
||||
input[type='checkbox'] + label::after,
|
||||
input[type='radio'] + label::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
// Unhide on the checked state
|
||||
input[type='checkbox']:checked + label::after,
|
||||
input[type='radio']:checked + label::after {
|
||||
content: '';
|
||||
}
|
||||
|
||||
input[type='checkbox']:focus + label::before,
|
||||
input[type='radio']:focus + label::before {
|
||||
@include focus;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
// Outer box of the fake checkbox
|
||||
label::before {
|
||||
height: var(--height-checkbox);
|
||||
width: var(--height-checkbox);
|
||||
border: 1px solid var(--color-input-border);
|
||||
border-radius: var(--border-radius);
|
||||
left: 0px;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
// Checkmark of the fake checkbox
|
||||
label::after {
|
||||
height: 6px;
|
||||
width: 12px;
|
||||
border-left: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-color: var(--color-input-toggle);
|
||||
border-left-color: var(--color-input-toggle);
|
||||
transform: rotate(-45deg);
|
||||
left: 6px;
|
||||
top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.radio {
|
||||
input[type='radio'] {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
// Outer box of the fake radio
|
||||
label::before {
|
||||
height: var(--height-radio);
|
||||
width: var(--height-radio);
|
||||
border: 1px solid var(--color-input-border);
|
||||
border-radius: calc(var(--height-radio) * 0.5);
|
||||
left: 0px;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
// Checkmark of the fake radio
|
||||
label::after {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-primary);
|
||||
left: 6px;
|
||||
top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.range__label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-bottom: var(--spacing-m);
|
||||
|
||||
> * {
|
||||
width: 33%;
|
||||
text-align: center;
|
||||
|
||||
&:first-of-type {
|
||||
text-align: left;
|
||||
}
|
||||
&:last-of-type {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fieldset-group {
|
||||
@extend fieldset-group;
|
||||
}
|
||||
|
||||
.fieldset-section {
|
||||
@extend fieldset-section;
|
||||
}
|
||||
|
||||
.input-submit {
|
||||
@extend input-submit;
|
||||
}
|
||||
|
||||
input-submit {
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
fieldset-group {
|
||||
+ fieldset-group {
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
&.fieldset-group--smushed {
|
||||
justify-content: flex-start;
|
||||
|
||||
fieldset-section {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
|
||||
&:first-child {
|
||||
input,
|
||||
select {
|
||||
border-right: none;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-of-type(2) {
|
||||
input,
|
||||
select {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-left: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.fieldgroup--paginate {
|
||||
padding-bottom: var(--spacing-l);
|
||||
margin-top: var(--spacing-l);
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
// This is a special case where the prefix appears "inside" the input
|
||||
// It would be way simpler to just use position: absolute and give it a width
|
||||
// but the width can change when we use it for the name prefix
|
||||
// lbry:// {input}, lbry://@short {input}, @lbry://longername {input}
|
||||
// The spacing/alignment isn't very robust and will probably need to be changed
|
||||
// if we use this in more places
|
||||
&.fieldset-group--disabled-prefix {
|
||||
align-items: flex-end;
|
||||
|
||||
label {
|
||||
min-height: 18px;
|
||||
white-space: nowrap;
|
||||
// Set width 0 and overflow visible so the label can act as if it's the input label and not a random text node in a side by side div
|
||||
overflow: visible;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
fieldset-section:first-child {
|
||||
max-width: 40%;
|
||||
|
||||
.form-field__prefix {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
padding: 0.5rem;
|
||||
height: var(--height-input);
|
||||
border: 1px solid;
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-color: var(--color-input-border);
|
||||
border-right-color: var(--color-input-prefix-border);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-input-prefix-bg);
|
||||
}
|
||||
}
|
||||
|
||||
fieldset-section:last-child {
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
// Overwrite the input's label to wrap instead. This is usually
|
||||
// an error message, which could be long in other languages.
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
input {
|
||||
border-left: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-color: var(--color-input-border);
|
||||
padding-left: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-field--copyable {
|
||||
padding: 0.2rem 0.75rem;
|
||||
text-overflow: ellipsis;
|
||||
user-select: text;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.form-field--short {
|
||||
width: 100%;
|
||||
@media (min-width: $breakpoint-small) {
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field--price-amount {
|
||||
max-width: 6em;
|
||||
}
|
||||
|
||||
.form-field--price-amount--auto {
|
||||
width: auto;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.form-field--address {
|
||||
min-width: 18em;
|
||||
@media (max-width: $breakpoint-xxsmall) {
|
||||
min-width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field__help {
|
||||
@extend .help;
|
||||
}
|
||||
|
||||
.form-field__help + .checkbox,
|
||||
.form-field__help + .radio {
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
|
||||
.form-field__conjuction {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.form-field__two-column {
|
||||
@media (min-width: $breakpoint-small) {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field__quick-action {
|
||||
float: right;
|
||||
font-size: var(--font-xsmall);
|
||||
margin-top: 2.5%;
|
||||
}
|
||||
|
||||
.form-field__textarea-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-xxs);
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.form-field__quick-emojis {
|
||||
> *:not(:last-child) {
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
fieldset-section {
|
||||
.form-field__internal-option {
|
||||
margin-top: var(--spacing-s);
|
||||
margin-left: 2.2rem;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: var(--spacing-s); // Extra specificity needed here since _section.scss is applied after this file
|
||||
}
|
||||
}
|
||||
|
||||
.select--slim {
|
||||
margin-bottom: var(--spacing-xxs);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
select {
|
||||
max-height: 1.5rem !important;
|
||||
padding: 0 var(--spacing-xs);
|
||||
padding-right: var(--spacing-l);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#automatic_dark_mode_range_start,
|
||||
#automatic_dark_mode_range_end {
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.date-picker-input {
|
||||
font-weight: bold;
|
||||
|
||||
.react-datetime-picker__wrapper {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field-date-picker {
|
||||
margin-bottom: var(--spacing-l);
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
|
||||
.date-picker-input,
|
||||
.button--link {
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
.react-datetime-picker__button {
|
||||
svg {
|
||||
stroke: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.react-datetime-picker__button:enabled:hover .react-datetime-picker__button__icon,
|
||||
.react-datetime-picker__button:enabled:focus .react-datetime-picker__button__icon {
|
||||
stroke: var(--color-primary);
|
||||
}
|
||||
|
||||
.react-date-picker__calendar {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.react-calendar {
|
||||
width: 350px;
|
||||
max-width: 100%;
|
||||
background: var(--color-card-background);
|
||||
border: 1px solid #a0a096;
|
||||
font-family: inherit;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.react-calendar--doubleView {
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
.react-calendar--doubleView .react-calendar__viewContainer {
|
||||
display: flex;
|
||||
margin: -0.5em;
|
||||
}
|
||||
|
||||
.react-calendar--doubleView .react-calendar__viewContainer > * {
|
||||
width: 50%;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.react-calendar,
|
||||
.react-calendar *,
|
||||
.react-calendar *:before,
|
||||
.react-calendar *:after {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding: 2px 1px;
|
||||
}
|
||||
|
||||
.react-calendar button {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-calendar button:enabled:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.react-calendar__navigation {
|
||||
height: 44px;
|
||||
margin-bottom: 1em;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__navigation__label {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__navigation button {
|
||||
min-width: 44px;
|
||||
background: none;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__navigation button:enabled:hover,
|
||||
.react-calendar__navigation button:enabled:focus {
|
||||
background: var(--color-button-alt-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar__navigation button[disabled] {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-size: 0.75em;
|
||||
color: var(--color-text-alt);
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays__weekday {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekNumbers {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekNumbers .react-calendar__tile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75em;
|
||||
padding: calc(0.75em / 0.75) calc(0.5em / 0.75);
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day,
|
||||
.react-calendar__month-view__days__day--weekend {
|
||||
color: var(--color-text);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth {
|
||||
color: var(--color-gray-5);
|
||||
}
|
||||
|
||||
.react-calendar__year-view .react-calendar__tile,
|
||||
.react-calendar__decade-view .react-calendar__tile,
|
||||
.react-calendar__century-view .react-calendar__tile {
|
||||
padding: 2em 0.5em;
|
||||
}
|
||||
|
||||
.react-calendar__tile {
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
padding: 0.75em 0.5em;
|
||||
background: none;
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__tile:enabled:hover,
|
||||
.react-calendar__tile:enabled:focus {
|
||||
background: var(--color-button-alt-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar__tile--now {
|
||||
background: var(--color-button-secondary-bg);
|
||||
}
|
||||
|
||||
.react-calendar__tile--now:enabled:hover,
|
||||
.react-calendar__tile--now:enabled:focus {
|
||||
background: var(--color-button-secondary-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar__tile--hasActive {
|
||||
color: var(--color-button-primary-text);
|
||||
background: var(--color-button-primary-bg);
|
||||
}
|
||||
|
||||
.react-calendar__tile--hasActive:enabled:hover,
|
||||
.react-calendar__tile--hasActive:enabled:focus {
|
||||
background: var(--color-button-primary-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar__tile--active {
|
||||
color: var(--color-button-primary-text);
|
||||
background: var(--color-button-primary-bg);
|
||||
}
|
||||
|
||||
.react-calendar__tile--active:enabled:hover,
|
||||
.react-calendar__tile--active:enabled:focus {
|
||||
background: var(--color-button-primary-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar--selectRange .react-calendar__tile--hover {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
.react-datetime-picker__inputGroup__amPm {
|
||||
background: var(--color-input-bg);
|
||||
}
|
||||
|
||||
.react-datetime-picker__inputGroup__leadingZero {
|
||||
// Not perfect, but good enough for our standard zoom levels.
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.react-datetime-picker__inputGroup__input--hasLeadingZero {
|
||||
margin-left: -0.54em;
|
||||
padding-left: calc(1px + 0.54em);
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth {
|
||||
color: var(--color-gray-5);
|
||||
}
|
||||
}
|
||||
|
||||
.form-field-calendar {
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--color-border);
|
||||
margin-left: calc(var(--spacing-xs) * -1);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
||||
box-shadow: 3px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
[theme='dark'] {
|
||||
// Color overrides
|
||||
--color-primary: #e50054;
|
||||
--color-primary-alt: #66001880;
|
||||
--color-fire: #ff6635;
|
||||
--color-fire-outside: #ff9b20;
|
||||
|
||||
// Structure
|
||||
--color-background: #140e1b;
|
||||
--color-background-overlay: #0c0d0e95;
|
||||
--color-border: #30243d;
|
||||
--color-card-background: #181021;
|
||||
--color-card-background-highlighted: #241c30;
|
||||
|
||||
// Text
|
||||
--color-text: var(--color-gray-1);
|
||||
--color-text-subtitle: var(--color-gray-4);
|
||||
--color-text-empty: var(--color-text-subtitle);
|
||||
--color-text-help: #bbbbbb;
|
||||
--color-text-warning: #212529;
|
||||
--color-text-warning--background: var(--lbry-yellow-1);
|
||||
--color-text-error: var(--color-danger);
|
||||
--color-error: var(--color-danger-alt);
|
||||
--color-blockquote: var(--color-gray-5);
|
||||
--color-blockquote-bg: var(--color-card-background-highlighted);
|
||||
--color-help-warning-text: var(--color-white-alt);
|
||||
--color-help-warning-bg: #fbbf2450;
|
||||
|
||||
// Header
|
||||
--color-header-button: #38274c;
|
||||
--color-header-background: #231830;
|
||||
|
||||
// Button
|
||||
--color-button-primary-text: white;
|
||||
--color-button-primary-hover-text: var(--color-primary-alt);
|
||||
--color-button-secondary-bg: #2c1543;
|
||||
--color-button-secondary-border: #4f1c82;
|
||||
--color-button-secondary-bg-hover: #3b1c5b;
|
||||
--color-button-secondary-text: #efefef;
|
||||
--color-button-alt-bg: var(--color-header-button);
|
||||
--color-button-alt-bg-hover: #2b2037;
|
||||
--color-button-toggle-text: var(--color-text);
|
||||
--color-button-toggle-bg: var(--color-primary-alt);
|
||||
--color-button-toggle-bg-hover: var(--color-primary-alt);
|
||||
--color-button-alt-text: #e2e9f0;
|
||||
--color-button-border: #5b4475;
|
||||
--color-link: var(--color-primary);
|
||||
--color-link-hover: #d75673;
|
||||
--color-link-active: #ec1d4c;
|
||||
--color-link-focus-bg: #3d2d4e;
|
||||
|
||||
// Input
|
||||
--color-input: #f4f4f5;
|
||||
--color-input-label: #a7a7a7;
|
||||
--color-input-placeholder: #f4f4f5;
|
||||
--color-input-bg: var(--color-header-button);
|
||||
--color-input-bg-copyable: #4c3861;
|
||||
--color-input-border: var(--color-border);
|
||||
--color-input-border-active: var(--color-secondary);
|
||||
--color-input-toggle: var(--color-primary-alt-3);
|
||||
--color-input-toggle-bg: var(--color-input-bg);
|
||||
--color-input-toggle-bg-hover: var(--color-secondary);
|
||||
--color-input-bg-selected: var(--color-primary-alt);
|
||||
--color-input-prefix-bg: var(--color-input-bg-copyable);
|
||||
--color-input-prefix-border: var(--color-gray-4);
|
||||
--select-toggle-background: url("data:image/svg+xml,%3Csvg viewBox='0 0 96 96' xmlns='http://www.w3.org/2000/svg' fill='%23ffffff'%3E%3Cpath d='M17.172, 31.172c1.562, -1.562 4.095, -1.562 5.656, 0l25.172, 25.171l25.172, -25.171c1.562, -1.562 4.095, -1.562 5.656, 0c1.562, 1.562 1.562, 4.095 0, 5.656l-28, 28c-1.562, 1.562 -4.095, 1.562 -5.656, 0l-28, -28c-0.781, -0.781 -1.172, -1.805 -1.172, -2.828c0, -1.023 0.391, -2.047 1.172, -2.828Z'/%3E%3C/svg%3E%0A");
|
||||
|
||||
// Navigation
|
||||
--color-navigation-icon: #76808a;
|
||||
--color-navigation-link: #b9c3ce;
|
||||
--color-navigation-active: #2b2037;
|
||||
--color-navigation-active-text: #c6bcd2;
|
||||
--color-navigation-hover: #21182a;
|
||||
--color-navigation-hover-text: #c6bcd2;
|
||||
|
||||
// Tags
|
||||
--color-tag: #ff85b1;
|
||||
--color-tag-bg: var(--color-navigation-hover);
|
||||
--color-tag-hover: var(--color-white);
|
||||
--color-tag-bg-hover: var(--color-primary-alt-2);
|
||||
--color-tag-mature-bg: var(--color-primary-alt-2);
|
||||
|
||||
// Menu
|
||||
--color-menu-background: var(--color-header-background);
|
||||
--color-menu-background--active: var(--color-primary-alt);
|
||||
--color-menu-icon: #928b9b;
|
||||
--color-menu-icon-active: #d6d6d6;
|
||||
|
||||
// Comments
|
||||
--color-comment-menu: #6a6a6a;
|
||||
--color-comment-menu-hovering: #e0e0e0;
|
||||
--color-comment-highlighted: #484734;
|
||||
--color-comment-threadline: #24192f;
|
||||
--color-comment-threadline-hover: var(--color-gray-4);
|
||||
|
||||
// Other
|
||||
--color-tab-text: var(--color-white);
|
||||
--color-tabs-background: var(--color-card-background);
|
||||
--color-tab-divider: var(--color-white);
|
||||
--color-modal-background: var(--color-card-background);
|
||||
--color-notice: #58563b;
|
||||
--color-purchased: #ffd580;
|
||||
--color-purchased-alt: var(--color-purchased);
|
||||
--color-purchased-text: black;
|
||||
--color-thumbnail-background: var(--color-gray-5);
|
||||
--color-tooltip-bg: #2f3337;
|
||||
--color-focus: #e91e6329;
|
||||
--color-placeholder-background: #261a35;
|
||||
--color-spinner-light: white;
|
||||
--color-spinner-dark: #212529;
|
||||
--color-login-graphic-background: var(--color-background);
|
||||
|
||||
// Editor
|
||||
--color-editor-cursor: var(--color-text);
|
||||
--color-editor-quote: #d3d3d3;
|
||||
--color-editor-tag: #efbe5d;
|
||||
--color-editor-attr: #68ccf9;
|
||||
--color-editor-string: #ff8b6b;
|
||||
--color-editor-inline-code-fg: #ce9178;
|
||||
--color-editor-inline-code-fg-preview: #e8b692;
|
||||
--color-editor-inline-code-bg: rgba(20, 68, 102, 0.3);
|
||||
--color-editor-inline-code-bg-preview: #464b50;
|
||||
--color-editor-selected: #264f78;
|
||||
--color-editor-link: var(--color-link);
|
||||
--color-editor-url: var(--color-editor-string);
|
||||
--color-editor-hr: var(--color-editor-tag);
|
||||
--color-editor-hr-preview: #a0a0a0;
|
||||
|
||||
// Ads
|
||||
--color-ads-background: #475057;
|
||||
--color-ads-text: #111;
|
||||
--color-ads-link: var(--color-primary-alt);
|
||||
|
||||
// Scrollbar
|
||||
--color-scrollbar-thumb-bg: rgba(255, 255, 255, 0.2);
|
||||
--color-scrollbar-track-bg: transparent;
|
||||
}
|
|
@ -1,200 +0,0 @@
|
|||
//
|
||||
// Colors are taken from this color palette
|
||||
// https://tailwindcss.com/docs/customizing-colors
|
||||
// New colors should be also taken from the same color palette (if possible)
|
||||
//
|
||||
:root {
|
||||
// Generic colors
|
||||
--color-primary: #047857;
|
||||
--color-primary-alt: #e4f4ef;
|
||||
--color-primary-alt-2: #065f46;
|
||||
--color-primary-alt-3: #10b981;
|
||||
--color-secondary: #1e3a8a;
|
||||
--color-secondary-alt: #dbeafe;
|
||||
--color-secondary-alt-2: #bfdbfe;
|
||||
--color-secondary-alt-3: #1e40af;
|
||||
--color-tertiary: #5b21b6;
|
||||
--color-tertiary-alt: #f5f3ff;
|
||||
--color-danger: #991b1b;
|
||||
--color-danger-alt: #fecaca;
|
||||
--color-warning: #fff58c;
|
||||
--color-black: #111;
|
||||
--color-white: #fdfdfd;
|
||||
--color-white-alt: #fafafa;
|
||||
--color-gray-1: #f3f4f6;
|
||||
--color-gray-2: #e5e7eb;
|
||||
--color-gray-3: #d1d5db;
|
||||
--color-gray-4: #9ca3af;
|
||||
--color-gray-5: #71717a;
|
||||
--color-gray-6: #52525b;
|
||||
--color-gray-7: #3f3f46;
|
||||
--color-gray-8: #27272a;
|
||||
--color-gray-9: #1f1f22;
|
||||
--color-gray-10: #18181b;
|
||||
--color-amber: #f26522;
|
||||
--color-orange: #fb923c;
|
||||
|
||||
// Structure
|
||||
--color-text: var(--color-black);
|
||||
--color-text-inverse: #fdfdfd;
|
||||
--color-background: #fafafa;
|
||||
--color-background--splash: #212529;
|
||||
--color-border: #ededed;
|
||||
--color-background-overlay: #21252980;
|
||||
--color-card-background: #ffffff;
|
||||
--color-card-background-highlighted: #f1f7fe;
|
||||
|
||||
// Text
|
||||
--color-text-selection-bg: var(--color-secondary-alt);
|
||||
--color-text-selection: var(--color-secondary);
|
||||
--color-text-empty: #999999;
|
||||
--color-text-help: #999999;
|
||||
--color-text-subtitle: #767676;
|
||||
--color-text-warning: #212529;
|
||||
--color-help-warning-bg: #fef3c7;
|
||||
--color-help-warning-text: #555555;
|
||||
--color-text-warning--background: var(--lbry-yellow-1);
|
||||
--color-blockquote: var(--color-gray-3);
|
||||
--color-text-error: var(--color-danger);
|
||||
--color-error: var(--color-danger-alt);
|
||||
--color-tooltip-bg: #222;
|
||||
--color-tooltip-text: #fafafa;
|
||||
|
||||
// Header
|
||||
--color-header-background: #ffffff;
|
||||
--color-header-button: var(--color-button-alt-bg);
|
||||
--color-header-button-active: var(--color-primary-alt);
|
||||
--color-header-button-hover: var(--color-primary-alt);
|
||||
|
||||
// Button
|
||||
--color-button-primary-bg: var(--color-primary);
|
||||
--color-button-primary-text: var(--color-primary-alt);
|
||||
--color-button-primary-bg-hover: var(--color-primary-alt-2);
|
||||
--color-button-primary-hover-text: var(--color-primary-alt);
|
||||
--color-button-secondary-bg: var(--color-secondary-alt);
|
||||
--color-button-secondary-border: var(--color-secondary-alt);
|
||||
--color-button-secondary-text: var(--color-secondary);
|
||||
--color-button-secondary-bg-hover: var(--color-secondary-alt-2);
|
||||
--color-button-alt-bg: var(--color-gray-1);
|
||||
--color-button-alt-text: var(--color-text);
|
||||
--color-button-alt-bg-hover: var(--color-gray-2);
|
||||
--color-button-toggle-text: var(--color-primary);
|
||||
--color-button-toggle-bg: var(--color-primary-alt);
|
||||
--color-button-border: var(--color-gray-3);
|
||||
--color-link: var(--color-primary);
|
||||
--color-link-hover: var(--color-black);
|
||||
--color-link-focus-bg: var(--color-gray-1);
|
||||
|
||||
// Input
|
||||
--color-input-color: var(--color-black);
|
||||
--color-input-label: var(--color-gray-5);
|
||||
--color-input-placeholder: var(--color-gray-8);
|
||||
--color-input-bg: var(--color-gray-1);
|
||||
--color-input-border: var(--color-border);
|
||||
--color-input-border-active: var(--color-secondary);
|
||||
--color-input-toggle: var(--color-secondary);
|
||||
--color-input-toggle-bg: var(--color-gray-1);
|
||||
--color-input-toggle-bg-hover: var(--color-secondary-alt);
|
||||
--color-input-prefix-bg: var(--color-gray-2);
|
||||
--color-input-prefix-border: var(--color-gray-5);
|
||||
--color-input-bg-selected: var(--color-primary-alt);
|
||||
--select-toggle-background: url("data:image/svg+xml,%3Csvg viewBox='0 0 96 96' xmlns='http://www.w3.org/2000/svg' fill='%23212529'%3E%3Cpath d='M17.172, 31.172c1.562, -1.562 4.095, -1.562 5.656, 0l25.172, 25.171l25.172, -25.171c1.562, -1.562 4.095, -1.562 5.656, 0c1.562, 1.562 1.562, 4.095 0, 5.656l-28, 28c-1.562, 1.562 -4.095, 1.562 -5.656, 0l-28, -28c-0.781, -0.781 -1.172, -1.805 -1.172, -2.828c0, -1.023 0.391, -2.047 1.172, -2.828Z'/%3E%3C/svg%3E%0A");
|
||||
|
||||
// Navigation
|
||||
--color-navigation-icon: var(--color-gray-5);
|
||||
--color-navigation-link: var(--color-gray-5);
|
||||
--color-navigation-active: var(--color-primary-alt);
|
||||
--color-navigation-active-text: var(--color-primary);
|
||||
--color-navigation-hover: var(--color-gray-1);
|
||||
--color-navigation-hover-text: var(--color-primary);
|
||||
|
||||
// Tags
|
||||
--color-tag: var(--color-gray-5);
|
||||
--color-tag-bg: var(--color-button-alt-bg);
|
||||
--color-tag-hover: var(--color-primary-alt);
|
||||
--color-tag-bg-hover: var(--color-button-primary-bg);
|
||||
|
||||
// Tags (words)
|
||||
--color-tag-words: var(--color-primary);
|
||||
--color-tag-words-bg: var(--color-primary-alt);
|
||||
--color-tag-words-hover: var(--color-primary);
|
||||
--color-tag-words-bg-hover: var(--color-primary-alt-3);
|
||||
|
||||
// Menu
|
||||
--color-menu-background: var(--color-header-background);
|
||||
--color-menu-background--active: var(--color-card-background-highlighted);
|
||||
--color-menu-icon: var(--color-navigation-link);
|
||||
--color-menu-icon-active: var(--color-navigation-link);
|
||||
|
||||
// Comments
|
||||
--color-comment-menu: var(--color-gray-3);
|
||||
--color-comment-menu-hovering: var(--color-gray-6);
|
||||
--color-comment-highlighted: #fff2d9;
|
||||
--color-comment-threadline: var(--color-gray-1);
|
||||
--color-comment-threadline-hover: var(--color-gray-4);
|
||||
|
||||
// Icons
|
||||
--color-follow-bg: #ffd4da;
|
||||
--color-follow-icon: #e2495e;
|
||||
--color-view-bg: var(--color-secondary-alt);
|
||||
--color-view-icon: var(--color-secondary);
|
||||
|
||||
// Snack
|
||||
--color-snack-bg: var(--color-primary);
|
||||
--color-snack: var(--color-white);
|
||||
--color-snack-bg-error: var(--color-danger);
|
||||
--color-snack-upgrade: var(--color-tertiary);
|
||||
|
||||
// Superchat
|
||||
--color-superchat-text: var(--color-black);
|
||||
--color-superchat: var(--color-cost);
|
||||
--color-superchat__light: #fcd34d50;
|
||||
--color-superchat-2: #fde68a;
|
||||
--color-superchat-3: #fef3c7;
|
||||
--color-superchat-3__light: #fef3c750;
|
||||
--color-superchat-4: #fffbeb;
|
||||
|
||||
// Editor
|
||||
--color-editor-cursor: var(--color-text);
|
||||
--color-editor-quote: #707070;
|
||||
--color-editor-tag: #ea9400;
|
||||
--color-editor-attr: #04b0f4;
|
||||
--color-editor-string: #ff7451;
|
||||
--color-editor-inline-code-fg: var(--color-text);
|
||||
--color-editor-inline-code-fg-preview: #2e3439;
|
||||
--color-editor-inline-code-bg: rgba(157, 161, 165, 0.3);
|
||||
--color-editor-inline-code-bg-preview: #d0e8ff;
|
||||
--color-editor-selected: #add6ff;
|
||||
--color-editor-link: var(--color-link);
|
||||
--color-editor-url: var(--color-editor-string);
|
||||
--color-editor-hr: var(--color-editor-tag);
|
||||
--color-editor-hr-preview: #cccccc;
|
||||
|
||||
// Other
|
||||
--color-focus: #bfdbfe;
|
||||
--color-notification: #cc190f;
|
||||
--color-live: #cc190f;
|
||||
--color-nag: var(--color-orange);
|
||||
--color-cost: #fcd34d;
|
||||
--color-notice: #fef3ca;
|
||||
--color-purchased: var(--color-cost);
|
||||
--color-purchased-alt: #ffebc2;
|
||||
--color-purchased-text: var(--color-gray-5);
|
||||
--color-thumbnail-background: var(--color-gray-1);
|
||||
--color-spinner-light: var(--color-white);
|
||||
--color-spinner-dark: var(--color-black);
|
||||
--color-placeholder-background: var(--color-gray-1);
|
||||
--color-file-viewer-background: var(--color-card-background);
|
||||
--color-tabs-background: var(--color-card-background);
|
||||
--color-tab-divider: var(--color-primary);
|
||||
--color-modal-background: var(--color-card-background);
|
||||
--color-login-graphic-background: var(--color-primary-alt);
|
||||
|
||||
// Ads
|
||||
--color-ads-background: #fae5ff;
|
||||
--color-ads-link: var(--color-link);
|
||||
|
||||
// Scrollbar
|
||||
--color-scrollbar-thumb-bg: rgba(0, 0, 0, 0.2);
|
||||
--color-scrollbar-track-bg: transparent;
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
:root {
|
||||
// Generic colors
|
||||
--color-primary: #257761;
|
||||
--color-primary-alt: #e4f4ef;
|
||||
--color-primary-alt-2: #4b8576;
|
||||
--color-secondary: #295284;
|
||||
--color-secondary-alt: #d9eaff;
|
||||
--color-tertiary: #552470;
|
||||
--color-tertiary-alt: #f7e8ff;
|
||||
--color-danger: #9b2023;
|
||||
--color-danger-alt: #fccdce;
|
||||
--color-warning: #fff58c;
|
||||
--color-cost: #ffd580;
|
||||
--color-focus: #93cff2;
|
||||
--color-notification: #f02849;
|
||||
--color-live: #cc190f;
|
||||
|
||||
--color-black: #111;
|
||||
--color-white: #fdfdfd;
|
||||
--color-white-alt: #fafafa;
|
||||
--color-gray-1: #eff1f4;
|
||||
--color-gray-2: #d8dde1;
|
||||
--color-gray-3: #ced4da;
|
||||
--color-gray-4: #abb1b7;
|
||||
--color-gray-5: #666a6d;
|
||||
|
||||
// Text
|
||||
--color-text: var(--color-black);
|
||||
--color-text-subtitle: var(--color-gray-5);
|
||||
--color-text-inverse: #fdfdfd;
|
||||
|
||||
// Components
|
||||
|
||||
// Button
|
||||
--color-button-primary-bg: var(--color-primary);
|
||||
--color-button-primary-text: var(--color-primary-alt);
|
||||
--color-button-primary-bg-hover: var(--color-primary-alt-2);
|
||||
--color-button-primary-hover-text: var(--color-primary-alt);
|
||||
--color-button-secondary-bg: var(--color-secondary-alt);
|
||||
--color-button-secondary-border: var(--color-secondary-alt);
|
||||
--color-button-secondary-text: var(--color-secondary);
|
||||
--color-button-secondary-bg-hover: #b9d0e9;
|
||||
--color-button-alt-bg: var(--color-gray-1);
|
||||
--color-button-alt-text: var(--color-text);
|
||||
--color-button-alt-bg-hover: var(--color-gray-2);
|
||||
--color-link: var(--color-primary);
|
||||
--color-link-hover: var(--color-black);
|
||||
|
||||
// Table
|
||||
--color-table-highlight: var(--color-white-alt);
|
||||
|
||||
// Tag
|
||||
--color-tag: var(--color-gray-5);
|
||||
--color-tag-bg: var(--color-button-alt-bg);
|
||||
--color-tag-hover: var(--color-button-alt-text);
|
||||
--color-tag-bg-hover: var(--color-button-alt-bg-hover);
|
||||
}
|
|
@ -1,521 +0,0 @@
|
|||
// Generic html styles used across the App
|
||||
// component specific styling should go in the component scss file
|
||||
|
||||
*::selection {
|
||||
background-color: var(--color-text-selection-bg);
|
||||
color: var(--color-text-selection);
|
||||
}
|
||||
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
html {
|
||||
@include font-sans;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow-x: hidden;
|
||||
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-background);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 1em;
|
||||
cursor: default;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 1px;
|
||||
background-color: var(--color-gray-2);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p,
|
||||
ol,
|
||||
ul {
|
||||
& + p,
|
||||
& + ul,
|
||||
& + ol {
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
li {
|
||||
position: relative;
|
||||
list-style-position: outside;
|
||||
margin: var(--spacing-xs) 0;
|
||||
margin-left: var(--spacing-s);
|
||||
margin-bottom: 0;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-left: var(--spacing-xl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ul--no-style {
|
||||
list-style: none;
|
||||
margin-bottom: 0;
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
dl {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
overflow-x: visible;
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
|
||||
dt {
|
||||
flex-basis: 50%;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dt__text {
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
|
||||
dd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-basis: 45%;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
|
||||
.help--warning {
|
||||
margin-bottom: 0;
|
||||
margin-top: var(--spacing-s);
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.dd__text {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dd__button {
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
|
||||
dt,
|
||||
dd {
|
||||
padding: var(--spacing-m) var(--spacing-s);
|
||||
border-top: 1px solid var(--color-border);
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 0 0.8rem;
|
||||
margin-top: var(--spacing-xxs);
|
||||
margin-bottom: var(--spacing-xxs);
|
||||
opacity: 0.9;
|
||||
border-left: 0.2rem solid var(--color-blockquote);
|
||||
color: var(--color-text-subtitle);
|
||||
}
|
||||
|
||||
code {
|
||||
@include font-mono;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
|
||||
img,
|
||||
a {
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
img {
|
||||
// Hide alt text when an image fails to load
|
||||
text-indent: -9999px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: calc(var(--height-input) * 2);
|
||||
}
|
||||
|
||||
.columns {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
> * {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 15rem;
|
||||
margin-bottom: var(--spacing-l);
|
||||
|
||||
&:first-child {
|
||||
flex-basis: 1px;
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
flex-direction: column;
|
||||
|
||||
& > * {
|
||||
margin: 0;
|
||||
margin-bottom: var(--spacing-m);
|
||||
width: 100%;
|
||||
flex-basis: auto;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
|
||||
.column__item:not(:first-child) {
|
||||
padding-left: $spacing-width * 2/3;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.column__item--between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
flex-direction: column;
|
||||
.column__item:not(:first-child) {
|
||||
padding-left: 0;
|
||||
flex: 1;
|
||||
}
|
||||
& > * {
|
||||
margin: 0;
|
||||
margin-bottom: var(--spacing-m);
|
||||
width: 100%;
|
||||
flex-basis: auto;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.truncated-text {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.busy-indicator__loader {
|
||||
min-width: 16px;
|
||||
min-height: 8px;
|
||||
margin: -1rem 0;
|
||||
padding: 0 30px;
|
||||
|
||||
background: url('../../static/img/busy.gif') no-repeat center center;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.help {
|
||||
display: block;
|
||||
font-size: var(--font-xsmall);
|
||||
color: var(--color-text-help);
|
||||
margin-top: var(--spacing-s);
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
|
||||
.button--link + .button--link {
|
||||
margin-left: var(--spacing-s);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
font-size: var(--font-small);
|
||||
}
|
||||
}
|
||||
|
||||
.help--warning {
|
||||
@extend .help;
|
||||
padding: var(--spacing-s);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-help-warning-bg);
|
||||
color: var(--color-help-warning-text);
|
||||
margin-bottom: var(--spacing-s);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.help--notice {
|
||||
@extend .help--warning;
|
||||
background-color: var(--color-card-background-highlighted);
|
||||
}
|
||||
|
||||
.help--inline {
|
||||
@extend .help;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.icon--help {
|
||||
top: 3px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.help--card-actions {
|
||||
@extend .help;
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
|
||||
.help--dt {
|
||||
@extend .help;
|
||||
display: inline-block;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.help--spendable {
|
||||
margin-left: var(--spacing-xxs);
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--color-text-empty);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.empty--centered {
|
||||
text-align: center;
|
||||
padding: calc(var(--spacing-l) * 3) 0;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
width: 134px;
|
||||
height: 134px;
|
||||
border: 3px solid white;
|
||||
|
||||
&.qr-code--right-padding {
|
||||
margin-right: $spacing-vertical * 2/3;
|
||||
}
|
||||
|
||||
&.qr-code--top-padding {
|
||||
margin-top: $spacing-vertical * 2/3;
|
||||
}
|
||||
}
|
||||
|
||||
.error__wrapper {
|
||||
background-color: var(--color-error);
|
||||
padding: var(--spacing-s);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.error__wrapper--no-overflow {
|
||||
@extend .error__wrapper;
|
||||
max-height: 10rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.error__text {
|
||||
color: var(--color-text-error);
|
||||
}
|
||||
|
||||
.help--error {
|
||||
@extend .help;
|
||||
color: var(--color-text-error);
|
||||
}
|
||||
|
||||
.thumbnail-preview {
|
||||
width: var(--thumbnail-preview-width);
|
||||
height: var(--thumbnail-preview-height);
|
||||
background-color: var(--color-thumbnail-background);
|
||||
padding: var(--spacing-s);
|
||||
font-size: var(--font-small);
|
||||
border-radius: var(--border-radius);
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.thumbnail-picker__preview {
|
||||
width: calc(var(--thumbnail-preview-width) * 1.5);
|
||||
height: calc(var(--thumbnail-preview-height) * 1.5);
|
||||
background-color: var(--color-thumbnail-background);
|
||||
padding: var(--spacing-s);
|
||||
font-size: var(--font-small);
|
||||
border-radius: var(--border-radius);
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.download-text {
|
||||
font-size: var(--font-xsmall);
|
||||
}
|
||||
|
||||
.notice-message {
|
||||
position: relative;
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--spacing-l);
|
||||
background-color: var(--color-primary-alt);
|
||||
|
||||
~ .card {
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
.notice-message--loud {
|
||||
@extend .notice-message;
|
||||
background-color: #fef1f6;
|
||||
color: var(--color-black);
|
||||
font-weight: bold;
|
||||
|
||||
.button {
|
||||
color: #fa6165;
|
||||
}
|
||||
}
|
||||
|
||||
.privacy-img {
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
.confirm__label {
|
||||
@extend label;
|
||||
}
|
||||
|
||||
.confirm__value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-m);
|
||||
font-size: var(--font-large);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm__value--no-gap {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.confirm__value--subitem {
|
||||
font-size: var(--font-xsmall);
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
display: none;
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-hidden {
|
||||
@media (max-width: $breakpoint-small) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ads-test {
|
||||
height: 50vh;
|
||||
position: relative;
|
||||
|
||||
.video-js {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.video-js .vjs-tech {
|
||||
height: 50vh;
|
||||
}
|
||||
}
|
||||
|
||||
.adspruce-bannerspot {
|
||||
height: 5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.release__notes {
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.signup__odysee-logo {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
margin-top: var(--spacing-xl);
|
||||
height: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.home__meme {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
font-size: 1rem;
|
||||
margin-bottom: var(--spacing-m);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: var(--spacing-l);
|
||||
}
|
||||
|
||||
> .button {
|
||||
white-space: initial;
|
||||
}
|
||||
}
|
|
@ -1,237 +0,0 @@
|
|||
@mixin between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@mixin breakpoint-max($breakpoint) {
|
||||
@media (max-width: #{$breakpoint}px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin breakpoint-min($breakpoint) {
|
||||
@media (min-width: #{$breakpoint}px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin center {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@mixin clearfix {
|
||||
clear: both;
|
||||
content: '';
|
||||
display: block;
|
||||
}
|
||||
|
||||
// (Smart) text truncation
|
||||
// Pass in a width to customize how much text is allowed
|
||||
// Omit value for basic text truncation
|
||||
@mixin constrict($value: null) {
|
||||
@if ($value) {
|
||||
max-width: $value;
|
||||
}
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@mixin create-grid($items-per-row: 4) {
|
||||
grid-template: repeat(1, 1fr) / repeat($items-per-row, 1fr);
|
||||
}
|
||||
|
||||
// Smart font include
|
||||
// Simply pass in the font-weight you want to use and the normal/italicized versions will be added
|
||||
// No more weighing down the front-end with references to unused weights
|
||||
@mixin font-face($font-weight, $relative-font-path, $font-name) {
|
||||
@font-face {
|
||||
font-family: $font-name;
|
||||
font-style: normal;
|
||||
font-weight: $font-weight;
|
||||
// sass-lint:disable indentation
|
||||
src: url('#{$relative-font-path}/#{$font-weight}.woff2') format('woff2'),
|
||||
url('#{$relative-font-path}/#{$font-weight}.woff') format('woff');
|
||||
// sass-lint:enable indentation
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: $font-name;
|
||||
font-style: italic;
|
||||
font-weight: $font-weight;
|
||||
// sass-lint:disable indentation
|
||||
src: url('#{$relative-font-path}/#{$font-weight}i.woff2') format('woff2'),
|
||||
url('#{$relative-font-path}/#{$font-weight}i.woff') format('woff');
|
||||
// sass-lint:enable indentation
|
||||
}
|
||||
}
|
||||
|
||||
@mixin font-mono {
|
||||
font-family: 'Fira Code', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
@mixin font-sans {
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
}
|
||||
|
||||
@mixin font-serif {
|
||||
font-family: Georgia, serif;
|
||||
}
|
||||
|
||||
@mixin hide-text {
|
||||
border: none;
|
||||
color: transparent;
|
||||
font: 0 / 0 a;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
// Cross-browser line-clamp support
|
||||
@mixin line-clamp($element-height: 2rem, $row-count: 2, $fade-color: var(--lbry-white), $computed-position: relative) {
|
||||
height: $element-height;
|
||||
overflow: hidden;
|
||||
position: $computed-position;
|
||||
|
||||
&::after {
|
||||
width: 50%;
|
||||
height: calc(#{$element-height} / #{$row-count});
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
background-image: linear-gradient(to right, rgba($lbry-white, 0), #{$fade-color} 80%);
|
||||
content: '';
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin no-user-select {
|
||||
user-select: none;
|
||||
|
||||
-ms-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
// Use CSS variables without upsetting Sass-Lint
|
||||
// https://github.com/sasstools/sass-lint/issues/1161#issuecomment-390537190
|
||||
@mixin root-prop($prop: null, $value: null) {
|
||||
@if ($prop and $value) {
|
||||
#{$prop}: $value;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin selection($background-color: var(--lbry-white), $text-color: var(--lbry-black)) {
|
||||
&::selection {
|
||||
background-color: $background-color;
|
||||
color: $text-color;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
&::-moz-selection {
|
||||
background-color: $background-color;
|
||||
color: $text-color;
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin thumbnail {
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::before {
|
||||
float: left;
|
||||
padding-top: var(--aspect-ratio-standard);
|
||||
}
|
||||
|
||||
&::after {
|
||||
clear: both;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin focus {
|
||||
box-shadow: 0 0 0 3px var(--color-focus);
|
||||
}
|
||||
|
||||
@mixin linkFocus {
|
||||
background-color: var(--color-link-focus-bg);
|
||||
box-shadow: 0 0 0 5px var(--color-link-focus-bg);
|
||||
}
|
||||
|
||||
@mixin underline($text-color: var(--lbry-black), $whitespace-color: var(--lbry-white)) {
|
||||
@include selection($text-color, $whitespace-color);
|
||||
|
||||
background-image: linear-gradient($whitespace-color, $whitespace-color),
|
||||
linear-gradient($whitespace-color, $whitespace-color), linear-gradient($text-color, $text-color);
|
||||
background-position: 0 88%, 100% 88%, 0 88%;
|
||||
background-repeat: no-repeat, no-repeat, repeat-x;
|
||||
background-size: 0.05rem 1px, 0.05rem 1px, 1px 1px;
|
||||
box-decoration-break: clone;
|
||||
display: inline;
|
||||
text-decoration: none;
|
||||
text-shadow: 0.03rem 0 $whitespace-color, -0.03rem 0 $whitespace-color, 0 0.03rem $whitespace-color,
|
||||
0 -0.03rem $whitespace-color, 0.06rem 0 $whitespace-color, -0.06rem 0 $whitespace-color, 0.09rem 0 $whitespace-color,
|
||||
-0.09rem 0 $whitespace-color, 0.12rem 0 $whitespace-color, -0.12rem 0 $whitespace-color, 0.15rem 0 $whitespace-color,
|
||||
-0.15rem 0 $whitespace-color;
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
// sass-lint:disable-line empty-args
|
||||
background-position: 0 90%, 100% 90%, 0 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin placeholder {
|
||||
animation: pulse 2s infinite ease-in-out;
|
||||
background-color: var(--color-placeholder-background);
|
||||
border-radius: var(--card-radius);
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
@mixin mediaThumbHoverZoom {
|
||||
.media__thumb,
|
||||
img {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.media__thumb,
|
||||
img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin handleClaimTileGifThumbnail($width) {
|
||||
.ff-canvas,
|
||||
.freezeframe-img {
|
||||
height: calc(#{$width} * (9 / 16)) !important;
|
||||
width: $width;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin handleClaimListGifThumbnail($width) {
|
||||
.ff-canvas,
|
||||
.freezeframe-img {
|
||||
height: calc(#{$width} * (9 / 16)) !important;
|
||||
width: $width;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin handleChannelGif($size) {
|
||||
height: $size;
|
||||
width: $size;
|
||||
|
||||
.ff-canvas,
|
||||
.freezeframe-img {
|
||||
border-radius: var(--border-radius);
|
||||
height: $size !important;
|
||||
width: $size !important;
|
||||
}
|
||||
}
|
|
@ -1,245 +0,0 @@
|
|||
html {
|
||||
box-sizing: border-box;
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
border: none;
|
||||
box-sizing: inherit;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
[disabled] {
|
||||
pointer-events: none;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
[readonly] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
[for],
|
||||
[role='button'],
|
||||
[type='button'],
|
||||
[type='checkbox'],
|
||||
[type='file'],
|
||||
[type='radio'],
|
||||
[type='select'],
|
||||
[type='submit'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a,
|
||||
area,
|
||||
button,
|
||||
[role='button'],
|
||||
input,
|
||||
label,
|
||||
select,
|
||||
summary,
|
||||
textarea {
|
||||
// Remove touch delay on supported devices
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: normal;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
|
||||
> li {
|
||||
list-style-position: inside;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
dd {
|
||||
width: 80%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
dl {
|
||||
width: 100%;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
dt {
|
||||
width: 20%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
line-height: inherit;
|
||||
|
||||
&:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--color-gray-1);
|
||||
}
|
||||
|
||||
input {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
|
||||
&::placeholder {
|
||||
color: inherit;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: var(--spacing-xxl);
|
||||
padding: var(--spacing-s);
|
||||
// border-color should be added in apps for blur/focus
|
||||
border: 1px solid;
|
||||
|
||||
&:not([disabled]) {
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
// sass-lint:disable-block no-important
|
||||
// Intelligent print styles
|
||||
pre,
|
||||
blockquote {
|
||||
border: 1px solid var(--color-gray-5) !important;
|
||||
page-break-inside: avoid !important;
|
||||
}
|
||||
|
||||
tr,
|
||||
img {
|
||||
page-break-inside: avoid !important;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 0.5cm !important;
|
||||
}
|
||||
|
||||
p,
|
||||
h2,
|
||||
h3 {
|
||||
orphans: 3 !important;
|
||||
widows: 3 !important;
|
||||
}
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
page-break-after: avoid !important;
|
||||
}
|
||||
|
||||
thead {
|
||||
display: table-header-group !important;
|
||||
}
|
||||
|
||||
// Faster, more stable printing
|
||||
* {
|
||||
background-color: transparent !important;
|
||||
background-image: none !important;
|
||||
color: var(--lbry-black) !important;
|
||||
filter: none !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
p {
|
||||
a {
|
||||
&[href]::after {
|
||||
// Show hypertext data for links and abbreviations
|
||||
content: ' (' attr(href) ')' !important;
|
||||
}
|
||||
|
||||
&[href^='javascript:'],
|
||||
&[href^='#'] {
|
||||
&::after {
|
||||
content: '' !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abbr {
|
||||
&[title]::after {
|
||||
content: ' (' attr(title) ')' !important;
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
abbr {
|
||||
text-decoration: underline !important;
|
||||
word-wrap: break-word !important;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
// Both of these should probably die and become variables as well
|
||||
$spacing-vertical: 2rem;
|
||||
$spacing-width: 36px;
|
||||
|
||||
$breakpoint-xxsmall: 450px;
|
||||
$breakpoint-xsmall: 600px;
|
||||
$breakpoint-small: 900px;
|
||||
$breakpoint-medium: 1150px;
|
||||
$breakpoint-large: 1600px;
|
||||
|
||||
:root {
|
||||
--border-radius: 10px;
|
||||
--height-input: 2.5rem;
|
||||
--height-button: 2.5rem;
|
||||
--height-checkbox: 24px;
|
||||
--height-radio: 24px;
|
||||
--height-badge: 24px;
|
||||
|
||||
// Spacing
|
||||
--spacing-xxs: calc(2rem / 5);
|
||||
--spacing-xs: calc(2rem / 4);
|
||||
--spacing-s: calc(2rem / 3);
|
||||
--spacing-m: calc(2rem / 2);
|
||||
--spacing-l: 2rem;
|
||||
--spacing-xl: 3rem;
|
||||
|
||||
// Aspect ratio
|
||||
--aspect-ratio-bluray: 41.6666666667%; // 12:5
|
||||
--aspect-ratio-panavision: 36.3636363636%; // 11:4
|
||||
--aspect-ratio-sd: 75%; // 4:3
|
||||
--aspect-ratio-standard: 56.25%; // 16:9
|
||||
|
||||
// Type
|
||||
--font-mono: 'Fira Code';
|
||||
--font-sans: Inter;
|
||||
--font-serif: Georgia;
|
||||
--font-weight-base: 400;
|
||||
--font-weight-light: 300;
|
||||
--font-weight-bold: 700;
|
||||
--font-base: 14px;
|
||||
--font-body: 1rem;
|
||||
--font-xxsmall: 0.65rem;
|
||||
--font-xsmall: 0.7344rem;
|
||||
--font-small: 0.8571rem;
|
||||
--font-large: 1.3rem;
|
||||
--font-title: 1.71rem;
|
||||
--font-heading: 2.94rem;
|
||||
|
||||
// Width & spacing
|
||||
--page-max-width: 1280px;
|
||||
--page-max-width--filepage: 1700px;
|
||||
--mac-titlebar-height: 24px;
|
||||
--mobile: 600px;
|
||||
--side-nav-width: 230px;
|
||||
--side-nav-width--micro: 125px;
|
||||
|
||||
--spacing-main-padding: var(--spacing-xl);
|
||||
--floating-viewer-width: 32rem;
|
||||
--floating-viewer-height: 18rem; // 32 * 9/16
|
||||
--floating-viewer-info-height: 5rem;
|
||||
--floating-viewer-container-height: calc(var(--floating-viewer-height) + var(--floating-viewer-info-height));
|
||||
--option-select-width: 8rem;
|
||||
|
||||
// Text
|
||||
--text-max-width: 660px;
|
||||
--text-link-padding: 4px;
|
||||
|
||||
// Tabs
|
||||
--tab-indicator-size: 0.5rem;
|
||||
|
||||
// Header
|
||||
// This is tied to the floating player so it knows where to attach to
|
||||
// ui/component/fileRenderFloating/view.jsx
|
||||
--header-height: 80px;
|
||||
|
||||
// Inline Player
|
||||
--inline-player-max-height: calc(100vh - var(--header-height) - var(--spacing-l) * 2);
|
||||
|
||||
// Card
|
||||
--card-radius: var(--border-radius);
|
||||
--card-max-width: 1000px;
|
||||
--card-box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
// Modal
|
||||
--modal-width: 550px;
|
||||
|
||||
// Animation :)
|
||||
--animation-duration: 0.2s;
|
||||
--animation-style: ease-in-out;
|
||||
|
||||
// Image
|
||||
--thumbnail-preview-height: 100px;
|
||||
--thumbnail-preview-width: 177px;
|
||||
--cover-photo-height: 210px;
|
||||
--channel-thumbnail-width: 10rem;
|
||||
--channel-thumbnail-width--small: 4rem;
|
||||
--file-list-thumbnail-width: 10rem;
|
||||
|
||||
--tag-height: 1.5rem;
|
||||
|
||||
--livestream-comments-width: 30rem;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
:root {
|
||||
--font-body: 0.8rem;
|
||||
}
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
:root {
|
||||
// Color overrides
|
||||
--color-primary: #fa6165;
|
||||
--color-primary-alt: #fef1f6;
|
||||
--color-primary-alt-2: #fb7e82;
|
||||
--color-primary-alt-3: #fbcbdd;
|
||||
--color-secondary: #f9902a;
|
||||
--color-secondary-alt: #fee8d2;
|
||||
--color-secondary-alt-2: #fefcf6;
|
||||
|
||||
// Structure
|
||||
--color-border: #ededed;
|
||||
--color-background: #fafafa;
|
||||
--color-background-overlay: #21252980;
|
||||
--color-card-background: #ffffff;
|
||||
--color-card-background-highlighted: #fff5f5;
|
||||
|
||||
// Text
|
||||
--color-text-selection-bg: var(--color-primary-alt);
|
||||
--color-text-selection: var(--color-primary);
|
||||
--color-text-error: var(--color-danger);
|
||||
--color-text-empty: #999999;
|
||||
--color-text-help: #999999;
|
||||
--color-text-subtitle: #767676;
|
||||
--color-text-warning: #212529;
|
||||
--color-help-warning-bg: #fef3c7;
|
||||
--color-text-warning--background: var(--lbry-yellow-1);
|
||||
--color-blockquote: var(--color-gray-3);
|
||||
--color-blockquote-bg: var(--color-gray-1);
|
||||
--color-tooltip-bg: #222;
|
||||
--color-tooltip-text: #fafafa;
|
||||
|
||||
// Header
|
||||
--color-header-button: var(--color-button-alt-bg);
|
||||
--color-header-background: #ffffff;
|
||||
|
||||
// Button
|
||||
--color-button-alt-bg: var(--color-gray-1);
|
||||
--color-button-alt-bg-hover: var(--color-gray-2);
|
||||
--color-button-alt-text: black;
|
||||
--color-button-primary-bg: var(--color-primary);
|
||||
--color-button-primary-bg-hover: var(--color-primary-alt-2);
|
||||
--color-button-primary-text: var(--color-primary-alt);
|
||||
--color-button-primary-hover-text: var(--color-white);
|
||||
--color-button-secondary-bg: var(--color-primary-alt);
|
||||
--color-button-secondary-border: var(--color-primary-alt-3);
|
||||
--color-button-secondary-text: var(--color-primary);
|
||||
--color-button-secondary-bg-hover: var(--color-primary-alt-3);
|
||||
--color-button-toggle-text: var(--color-primary);
|
||||
--color-button-toggle-bg: var(--color-primary-alt);
|
||||
--color-button-toggle-bg-hover: var(--color-primary-alt);
|
||||
--color-button-border: var(--color-gray-3);
|
||||
--color-link-active: var(--color-primary);
|
||||
--color-link-focus-bg: #f1f1f1;
|
||||
--color-link: var(--color-primary);
|
||||
|
||||
// Input
|
||||
--color-input-bg-selected: var(--color-primary-alt);
|
||||
--color-input-color: #111111;
|
||||
--color-input-label: var(--color-gray-5);
|
||||
--color-input-placeholder: #212529;
|
||||
--color-input-bg: var(--color-gray-1);
|
||||
--color-input-border: var(--color-border);
|
||||
--color-input-border-active: var(--color-secondary);
|
||||
--color-input-toggle: var(--color-secondary);
|
||||
--color-input-toggle-bg: var(--color-gray-1);
|
||||
--color-input-toggle-bg-hover: var(--color-secondary-alt);
|
||||
--color-input-prefix-bg: var(--color-gray-2);
|
||||
--color-input-prefix-border: var(--color-gray-5);
|
||||
--select-toggle-background: url("data:image/svg+xml,%3Csvg viewBox='0 0 96 96' xmlns='http://www.w3.org/2000/svg' fill='%23212529'%3E%3Cpath d='M17.172, 31.172c1.562, -1.562 4.095, -1.562 5.656, 0l25.172, 25.171l25.172, -25.171c1.562, -1.562 4.095, -1.562 5.656, 0c1.562, 1.562 1.562, 4.095 0, 5.656l-28, 28c-1.562, 1.562 -4.095, 1.562 -5.656, 0l-28, -28c-0.781, -0.781 -1.172, -1.805 -1.172, -2.828c0, -1.023 0.391, -2.047 1.172, -2.828Z'/%3E%3C/svg%3E%0A");
|
||||
|
||||
// Navigation
|
||||
--color-navigation-icon: var(--color-gray-5);
|
||||
--color-navigation-link: var(--color-gray-5);
|
||||
--color-navigation-active: var(--color-primary-alt);
|
||||
--color-navigation-active-text: var(--color-primary);
|
||||
--color-navigation-hover: var(--color-gray-1);
|
||||
--color-navigation-hover-text: #3f3f3f;
|
||||
|
||||
// Tags
|
||||
--color-tag: var(--color-primary-alt-2);
|
||||
--color-tag-bg: #f9f6f7;
|
||||
--color-tag-hover: var(--color-button-alt-text);
|
||||
--color-tag-bg-hover: var(--color-button-alt-bg-hover);
|
||||
|
||||
// Menu
|
||||
--color-menu-background: var(--color-header-background);
|
||||
--color-menu-icon: var(--color-navigation-link);
|
||||
--color-menu-icon-active: var(--color-navigation-link);
|
||||
--color-menu-background--selected: var(--color-secondary-alt);
|
||||
--color-menu-background--active: var(--color-primary-alt);
|
||||
|
||||
// Comments
|
||||
--color-comment-menu: #e0e0e0;
|
||||
--color-comment-menu-hovering: #6a6a6a;
|
||||
--color-comment-highlighted: #fff2d9;
|
||||
--color-comment-threadline: var(--color-gray-1);
|
||||
--color-comment-threadline-hover: var(--color-gray-4);
|
||||
|
||||
// Superchat
|
||||
--color-superchat-text: var(--color-black);
|
||||
--color-superchat: #fcd34d;
|
||||
--color-superchat__light: #fcd34d50;
|
||||
--color-superchat-2: #fde68a;
|
||||
--color-superchat-3: #fef3c7;
|
||||
--color-superchat-3__light: #fef3c750;
|
||||
--color-superchat-4: #fffbeb;
|
||||
|
||||
// Color
|
||||
--color-focus: #8dbff0;
|
||||
--color-nag: #fa8700;
|
||||
--color-error: #fcafca;
|
||||
--color-notice: #fef3ca;
|
||||
--color-purchased: var(--color-cost);
|
||||
--color-purchased-alt: #ffebc2;
|
||||
--color-purchased-text: black;
|
||||
--color-thumbnail-background: var(--color-gray-1);
|
||||
--color-spinner-light: #ffffff;
|
||||
--color-spinner-dark: #212529;
|
||||
--color-placeholder-background: #f0f0f0;
|
||||
--color-file-viewer-background: var(--color-card-background);
|
||||
--color-tabs-background: var(--color-card-background);
|
||||
--color-tab-divider: var(--color-primary);
|
||||
--color-modal-background: var(--color-card-background);
|
||||
|
||||
// Icons
|
||||
--color-follow-bg: #ffd4da;
|
||||
--color-follow-icon: #e2495e;
|
||||
--color-view-bg: var(--color-secondary-alt);
|
||||
--color-view-icon: var(--color-secondary);
|
||||
|
||||
// Editor
|
||||
--color-editor-cursor: var(--color-text);
|
||||
--color-editor-quote: #707070;
|
||||
--color-editor-tag: #ea9400;
|
||||
--color-editor-attr: #04b0f4;
|
||||
--color-editor-string: #ff7451;
|
||||
--color-editor-inline-code-fg: var(--color-text);
|
||||
--color-editor-inline-code-fg-preview: #2e3439;
|
||||
--color-editor-inline-code-bg: rgba(157, 161, 165, 0.3);
|
||||
--color-editor-inline-code-bg-preview: #d0e8ff;
|
||||
--color-editor-selected: #add6ff;
|
||||
--color-editor-link: var(--color-link);
|
||||
--color-editor-url: var(--color-editor-string);
|
||||
--color-editor-hr: var(--color-editor-tag);
|
||||
--color-editor-hr-preview: #cccccc;
|
||||
|
||||
// Ads
|
||||
--color-ads-background: #fae5ff;
|
||||
--color-ads-link: var(--color-link);
|
||||
|
||||
// Scrollbar
|
||||
--color-scrollbar-thumb-bg: rgba(0, 0, 0, 0.2);
|
||||
--color-scrollbar-track-bg: transparent;
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
// @flow
|
||||
/*
|
||||
https://api.na-backend.odysee.com/api/v1/proxy currently expects publish to consist
|
||||
of a multipart/form-data POST request with:
|
||||
- 'file' binary
|
||||
- 'json_payload' collection of publish params to be passed to the server's sdk.
|
||||
*/
|
||||
import { X_LBRY_AUTH_TOKEN } from '../../ui/constants/token';
|
||||
import { doUpdateUploadProgress } from 'lbryinc';
|
||||
|
||||
// A modified version of Lbry.apiCall that allows
|
||||
// to perform calling methods at arbitrary urls
|
||||
// and pass form file fields
|
||||
export default function apiPublishCallViaWeb(
|
||||
apiCall: (any, any, any, any) => any,
|
||||
connectionString: string,
|
||||
token: string,
|
||||
method: string,
|
||||
params: { file_path: string, preview: boolean, remote_url?: string }, // new param for remoteUrl
|
||||
resolve: Function,
|
||||
reject: Function
|
||||
) {
|
||||
const { file_path: filePath, preview, remote_url: remoteUrl } = params;
|
||||
|
||||
if (!filePath && !remoteUrl) {
|
||||
return apiCall(method, params, resolve, reject);
|
||||
}
|
||||
|
||||
const counter = new Date().getTime();
|
||||
let fileField = filePath;
|
||||
|
||||
if (preview) {
|
||||
// Send dummy file for the preview. The tx-fee calculation does not depend on it.
|
||||
const dummyContent = 'x';
|
||||
fileField = new File([dummyContent], 'dummy.md', { type: 'text/markdown' });
|
||||
}
|
||||
|
||||
// Putting a dummy value here, the server is going to process the POSTed file
|
||||
// and set the file_path itself
|
||||
|
||||
const body = new FormData();
|
||||
if (fileField) {
|
||||
body.append('file', fileField);
|
||||
params.file_path = '__POST_FILE__';
|
||||
delete params['remote_url'];
|
||||
} else if (remoteUrl) {
|
||||
body.append('remote_url', remoteUrl);
|
||||
delete params['remote_url'];
|
||||
}
|
||||
|
||||
const jsonPayload = JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
params,
|
||||
id: counter,
|
||||
});
|
||||
// no fileData? do the livestream remote publish
|
||||
body.append('json_payload', jsonPayload);
|
||||
|
||||
function makeRequest(connectionString, method, token, body, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open(method, connectionString);
|
||||
xhr.setRequestHeader(X_LBRY_AUTH_TOKEN, token);
|
||||
xhr.responseType = 'json';
|
||||
xhr.upload.onprogress = (e) => {
|
||||
let percentComplete = Math.ceil((e.loaded / e.total) * 100);
|
||||
window.store.dispatch(doUpdateUploadProgress(percentComplete, params, xhr));
|
||||
};
|
||||
xhr.onload = () => {
|
||||
window.store.dispatch(doUpdateUploadProgress(undefined, params));
|
||||
resolve(xhr);
|
||||
};
|
||||
xhr.onerror = () => {
|
||||
window.store.dispatch(doUpdateUploadProgress(undefined, params));
|
||||
reject(new Error(__('There was a problem with your upload. Please try again.')));
|
||||
};
|
||||
|
||||
xhr.onabort = () => {
|
||||
window.store.dispatch(doUpdateUploadProgress(undefined, params));
|
||||
};
|
||||
xhr.send(body);
|
||||
});
|
||||
}
|
||||
|
||||
return makeRequest(connectionString, 'POST', token, body, params)
|
||||
.then((xhr) => {
|
||||
let error;
|
||||
if (xhr && xhr.response) {
|
||||
if (xhr.status >= 200 && xhr.status < 300 && !xhr.response.error) {
|
||||
return resolve(xhr.response.result);
|
||||
} else if (xhr.response.error) {
|
||||
error = new Error(xhr.response.error.message);
|
||||
} else {
|
||||
error = new Error(__('Upload likely timed out. Try a smaller file while we work on this.'));
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
const PAGES = require('../../ui/constants/pages');
|
||||
|
||||
// Uncomment as you add metadata
|
||||
|
||||
module.exports.CATEGORY_METADATA = {
|
||||
[PAGES.BIG_HITS]: {
|
||||
title: 'Big Hits',
|
||||
description: 'Animation, pop culture, comedy, and all the other weird on Odysee',
|
||||
image: '',
|
||||
},
|
||||
[PAGES.COMMUNITY]: {
|
||||
title: 'The Universe',
|
||||
description: 'Podcasts, life, learning, and everything else on Odysee',
|
||||
image: '',
|
||||
},
|
||||
// [PAGES.ENLIGHTENMENT]: {
|
||||
// title: '',
|
||||
// description: '',
|
||||
// image: '',
|
||||
// },
|
||||
[PAGES.FINANCE]: {
|
||||
title: 'Finance 2.0',
|
||||
description: 'Crypto, Money, Economics, Markets on Odysee ',
|
||||
image: 'https://spee.ch/category-finance:c.jpg?quality=80&height=1200&width=630',
|
||||
},
|
||||
[PAGES.GAMING]: {
|
||||
title: 'Gaming',
|
||||
description: 'Pew pew bzzz gaming on Odysee',
|
||||
image: 'https://spee.ch/category-gaming:5.jpg?quality=80&height=1200&width=630',
|
||||
},
|
||||
[PAGES.GENERAL]: {
|
||||
title: 'Cheese',
|
||||
description: 'Cheese is the answer to life, the universe, and everything. We have primo cheese on Odysee',
|
||||
image: 'https://spee.ch/category-primary1:5.jpg?quality=80&height=1200&width=630',
|
||||
},
|
||||
[PAGES.LAB]: {
|
||||
title: 'Lab',
|
||||
description: 'Science - the real kind, on Odysee',
|
||||
image: '',
|
||||
},
|
||||
[PAGES.NEWS]: {
|
||||
title: 'News & Politics',
|
||||
description: `Stay up to date with all that's happening around the world on Odysee`,
|
||||
image: '',
|
||||
},
|
||||
[PAGES.MOVIES]: {
|
||||
title: 'Movies',
|
||||
description: `Do you love B rated movies? We've got you covered on Odysee`,
|
||||
image: 'https://spee.ch/category-movies:2.jpg?quality=80&height=1200&width=630',
|
||||
},
|
||||
[PAGES.MUSIC]: {
|
||||
title: 'Music',
|
||||
description: 'All the songs, reviews, covers, and how-tos you love on Odysee',
|
||||
image: 'https://spee.ch/category-music:8.jpg?quality=80&height=1200&width=630',
|
||||
},
|
||||
[PAGES.TECH]: {
|
||||
title: 'Tech',
|
||||
description: 'Hardware, software, startups, photography on Odysee',
|
||||
image: '',
|
||||
},
|
||||
[PAGES.TECHNOLOGY]: {
|
||||
title: 'Tech',
|
||||
description: 'Hardware, software, startups, photography on Odysee',
|
||||
image: '',
|
||||
},
|
||||
[PAGES.WILD_WEST]: {
|
||||
title: 'Wild West',
|
||||
description: 'Boosted by user credits, this is what the community promotes on Odysee',
|
||||
image: 'https://spee.ch/category-wildwest:1.jpg?quality=80&height=1200&width=630',
|
||||
},
|
||||
};
|
|
@ -1,60 +0,0 @@
|
|||
const mysql = require('mysql');
|
||||
|
||||
const pool = mysql.createPool({
|
||||
connectionLimit: 100,
|
||||
host: 'chainquery.lbry.com',
|
||||
user: 'lbrytv',
|
||||
password: process.env.CHAINQUERY_MYSQL_PASSWORD,
|
||||
database: 'chainquery',
|
||||
});
|
||||
|
||||
function queryPool(sql, params) {
|
||||
return new Promise((resolve) => {
|
||||
pool.query(sql, params, (error, rows) => {
|
||||
if (error) {
|
||||
console.log('error', error); // eslint-disable-line
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.getClaim = async function getClaim(claimName, claimId, channelName, channelClaimId) {
|
||||
let params = [claimName];
|
||||
|
||||
let sql =
|
||||
'SELECT channel_claim.name as channel, claim.claim_id, claim.name, claim.description, claim.language, claim.thumbnail_url, claim.title, claim.source_media_type, claim.frame_width, claim.frame_height, claim.fee, claim.release_time, claim.duration, claim.audio_duration, ' +
|
||||
'repost_channel.name as repost_channel, reposted_claim.claim_id as reposted_claim_id, reposted_claim.name as reposted_name, reposted_claim.description as reposted_description, reposted_claim.language as reposted_language, reposted_claim.thumbnail_url as reposted_thumbnail_url, reposted_claim.title as reposted_title, reposted_claim.source_media_type as reposted_source_media_type, reposted_claim.frame_width as reposted_frame_width, reposted_claim.frame_height as reposted_frame_height, reposted_claim.fee as reposted_fee ' +
|
||||
'FROM claim ' +
|
||||
'LEFT JOIN claim channel_claim on claim.publisher_id = channel_claim.claim_id ' +
|
||||
'LEFT JOIN claim as reposted_claim on reposted_claim.claim_id = claim.claim_reference ' +
|
||||
'AND (reposted_claim.bid_state in ("controlling", "active", "accepted", "spent")) ' +
|
||||
'LEFT JOIN claim as repost_channel on repost_channel.claim_id = reposted_claim.publisher_id ' +
|
||||
'WHERE claim.name = ?';
|
||||
|
||||
if (claimId) {
|
||||
sql += ' AND claim.claim_id LIKE ?';
|
||||
params.push(claimId + '%');
|
||||
sql += ' AND claim.bid_state in ("controlling", "active", "accepted", "spent")';
|
||||
} else {
|
||||
sql += ' AND claim.bid_state in ("controlling", "active", "accepted")';
|
||||
}
|
||||
|
||||
if (claimName[0] !== '@' && channelName) {
|
||||
sql += ' AND channel_claim.name = ?';
|
||||
params.push('@' + channelName);
|
||||
if (channelClaimId) {
|
||||
sql += ' AND channel_claim.claim_id LIKE ?';
|
||||
params.push(channelClaimId + '%');
|
||||
} else {
|
||||
sql += ' AND channel_claim.bid_state in ("controlling", "active", "accepted", "spent")';
|
||||
}
|
||||
}
|
||||
|
||||
sql += ' ORDER BY claim.bid_state DESC LIMIT 1';
|
||||
|
||||
return queryPool(sql, params);
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
const memo = {};
|
||||
// this didn't seem to help.
|
||||
if (!memo.homepageData) {
|
||||
try {
|
||||
memo.homepageData = require('../../custom/homepages/v2');
|
||||
} catch (err) {
|
||||
console.log('homepage data failed');
|
||||
}
|
||||
}
|
||||
const getHomepageJSON = () => {
|
||||
return memo.homepageData || {};
|
||||
};
|
||||
module.exports = { getHomepageJSON };
|
370
web/src/html.js
370
web/src/html.js
|
@ -1,370 +0,0 @@
|
|||
const {
|
||||
URL,
|
||||
DOMAIN,
|
||||
SITE_TITLE,
|
||||
SITE_CANONICAL_URL,
|
||||
OG_HOMEPAGE_TITLE,
|
||||
OG_TITLE_SUFFIX,
|
||||
OG_IMAGE_URL,
|
||||
SITE_DESCRIPTION,
|
||||
SITE_NAME,
|
||||
FAVICON,
|
||||
LBRY_WEB_API,
|
||||
} = require('../../config.js');
|
||||
|
||||
const { lbryProxy: Lbry } = require('../lbry');
|
||||
const { generateEmbedUrl, generateStreamUrl, generateDirectUrl } = require('../../ui/util/web');
|
||||
const PAGES = require('../../ui/constants/pages');
|
||||
const { CATEGORY_METADATA } = require('./category-metadata');
|
||||
const { parseURI, normalizeURI } = require('./lbryURI');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const moment = require('moment');
|
||||
const removeMd = require('remove-markdown');
|
||||
const { getJsBundleId } = require('../bundle-id.js');
|
||||
const jsBundleId = getJsBundleId();
|
||||
const SDK_API_PATH = `${LBRY_WEB_API}/api/v1`;
|
||||
const PROXY_URL = `${SDK_API_PATH}/proxy`;
|
||||
Lbry.setDaemonConnectionString(PROXY_URL);
|
||||
|
||||
function insertToHead(fullHtml, htmlToInsert) {
|
||||
const beginStr = '<!-- VARIABLE_HEAD_BEGIN -->';
|
||||
const finalStr = '<!-- VARIABLE_HEAD_END -->';
|
||||
|
||||
const beginIndex = fullHtml.indexOf(beginStr);
|
||||
const finalIndex = fullHtml.indexOf(finalStr);
|
||||
|
||||
if (beginIndex > -1 && finalIndex > -1 && finalIndex > beginIndex) {
|
||||
return `${fullHtml.slice(0, beginIndex)}${
|
||||
htmlToInsert || buildOgMetadata()
|
||||
}<script src="/public/ui-${jsBundleId}.js" async></script>${fullHtml.slice(finalIndex + finalStr.length)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function truncateDescription(description, maxChars = 200) {
|
||||
// Get list of single-codepoint strings
|
||||
const chars = [...description];
|
||||
// Use slice array instead of substring to prevent breaking emojis
|
||||
let truncated = chars.slice(0, maxChars).join('');
|
||||
// Format truncated string
|
||||
return chars.length > maxChars ? truncated + '...' : truncated;
|
||||
}
|
||||
|
||||
function normalizeClaimUrl(url) {
|
||||
return normalizeURI(url.replace(/:/g, '#'));
|
||||
}
|
||||
|
||||
function escapeHtmlProperty(property) {
|
||||
return property
|
||||
? String(property)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
: '';
|
||||
}
|
||||
|
||||
function getCategoryMeta(path) {
|
||||
const page = Object.keys(CATEGORY_METADATA).find((x) => path === `/$/${x}` || path === `/$/${x}/`);
|
||||
return CATEGORY_METADATA[page];
|
||||
}
|
||||
|
||||
//
|
||||
// Normal metadata with option to override certain values
|
||||
//
|
||||
function buildOgMetadata(overrideOptions = {}) {
|
||||
const { title, description, image, path } = overrideOptions;
|
||||
const cleanDescription = removeMd(description || SITE_DESCRIPTION);
|
||||
const head =
|
||||
`<title>${SITE_TITLE}</title>\n` +
|
||||
`<meta name="description" content="${cleanDescription}" />\n` +
|
||||
`<meta property="og:url" content="${path ? `${URL}${path}` : URL}" />\n` +
|
||||
`<meta property="og:title" content="${title || OG_HOMEPAGE_TITLE || SITE_TITLE}" />\n` +
|
||||
`<meta property="og:site_name" content="${SITE_NAME || SITE_TITLE}"/>\n` +
|
||||
`<meta property="og:description" content="${cleanDescription}" />\n` +
|
||||
`<meta property="og:image" content="${image || OG_IMAGE_URL || `${URL}/public/v2-og.png`}" />\n` +
|
||||
`<meta property="og:type" content="website"/>\n` +
|
||||
'<meta name="twitter:card" content="summary_large_image"/>\n' +
|
||||
`<meta name="twitter:title" content="${
|
||||
(title && title + ' ' + OG_TITLE_SUFFIX) || OG_HOMEPAGE_TITLE || SITE_TITLE
|
||||
}" />\n` +
|
||||
`<meta name="twitter:description" content="${cleanDescription}" />\n` +
|
||||
`<meta name="twitter:image" content="${image || OG_IMAGE_URL || `${URL}/public/v2-og.png`}"/>\n` +
|
||||
'<meta property="fb:app_id" content="1673146449633983" />\n' +
|
||||
`<link rel="canonical" content="${SITE_CANONICAL_URL || URL}"/>` +
|
||||
`<link rel="search" type="application/opensearchdescription+xml" title="${
|
||||
SITE_NAME || SITE_TITLE
|
||||
}" href="${URL}/opensearch.xml">`;
|
||||
return head;
|
||||
}
|
||||
|
||||
function conditionallyAddPWA() {
|
||||
let head = '';
|
||||
if (DOMAIN === 'odysee.com') {
|
||||
head += '<link rel="manifest" href="./public/pwa/manifest.json"/>';
|
||||
head += '<link rel="apple-touch-icon" sizes="180x180" href="./public/pwa/icon-180.png">';
|
||||
head += '<script src="./serviceWorker.js"></script>';
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
function addFavicon() {
|
||||
let head = '';
|
||||
head += `<link rel="icon" type="image/png" href="${FAVICON || './public/favicon.png'}" />`;
|
||||
return head;
|
||||
}
|
||||
|
||||
function buildHead() {
|
||||
const head =
|
||||
'<!-- VARIABLE_HEAD_BEGIN -->' +
|
||||
addFavicon() +
|
||||
conditionallyAddPWA() +
|
||||
buildOgMetadata() +
|
||||
'<!-- VARIABLE_HEAD_END -->';
|
||||
return head;
|
||||
}
|
||||
|
||||
function buildBasicOgMetadata() {
|
||||
const head = '<!-- VARIABLE_HEAD_BEGIN -->' + addFavicon() + buildOgMetadata() + '<!-- VARIABLE_HEAD_END -->';
|
||||
return head;
|
||||
}
|
||||
|
||||
//
|
||||
// Metadata used for urls that need claim information
|
||||
// Also has option to override defaults
|
||||
//
|
||||
function buildClaimOgMetadata(uri, claim, overrideOptions = {}) {
|
||||
// Initial setup for claim based og metadata
|
||||
const { claimName } = parseURI(uri);
|
||||
const { meta, value, signing_channel } = claim;
|
||||
const fee = value && value.fee && (Number(value.fee.amount) || 0);
|
||||
const tags = value && value.tags;
|
||||
const media = value && (value.video || value.audio || value.image);
|
||||
const source = value && value.source;
|
||||
const channel = signing_channel && signing_channel.name;
|
||||
const thumbnail = value && value.thumbnail && value.thumbnail.url;
|
||||
const mediaType = source && source.media_type;
|
||||
const mediaDuration = media && media.duration;
|
||||
const claimTitle = escapeHtmlProperty((value && value.title) || claimName);
|
||||
const releaseTime = (value && value.release_time) || (meta && meta.creation_timestamp) || 0;
|
||||
|
||||
const claimDescription =
|
||||
value && value.description && value.description.length > 0
|
||||
? escapeHtmlProperty(truncateDescription(value.description))
|
||||
: `View ${claimTitle} on ${SITE_NAME}`;
|
||||
|
||||
const claimLanguage =
|
||||
value && value.languages && value.languages.length > 0 ? escapeHtmlProperty(value.languages[0]) : 'en_US';
|
||||
|
||||
let imageThumbnail;
|
||||
|
||||
if (fee <= 0 && mediaType && mediaType.startsWith('image/')) {
|
||||
imageThumbnail = generateStreamUrl(claim.name, claim.claim_id);
|
||||
}
|
||||
|
||||
const claimThumbnail = escapeHtmlProperty(thumbnail) || imageThumbnail || OG_IMAGE_URL || `${URL}/public/v2-og.png`;
|
||||
|
||||
// Allow for ovverriding default claim based og metadata
|
||||
const title = overrideOptions.title || claimTitle;
|
||||
const description = overrideOptions.description || claimDescription;
|
||||
const cleanDescription = removeMd(description);
|
||||
|
||||
let head = '';
|
||||
|
||||
head += `${addFavicon()}`;
|
||||
head += '<meta charset="utf8"/>';
|
||||
head += `<title>${title}</title>`;
|
||||
head += `<meta name="description" content="${cleanDescription}"/>`;
|
||||
|
||||
if (tags && tags.length > 0) {
|
||||
head += `<meta name="keywords" content="${tags.toString()}"/>`;
|
||||
}
|
||||
|
||||
head += `<meta name="twitter:image" content="${claimThumbnail}"/>`;
|
||||
head += `<meta property="og:description" content="${cleanDescription}"/>`;
|
||||
head += `<meta property="og:image" content="${claimThumbnail}"/>`;
|
||||
head += `<meta property="og:locale" content="${claimLanguage}"/>`;
|
||||
head += `<meta property="og:site_name" content="${SITE_NAME}"/>`;
|
||||
head += `<meta property="og:type" content="website"/>`;
|
||||
head += `<meta property="og:title" content="${title}"/>`;
|
||||
head += `<meta name="twitter:title" content="${title}"/>`;
|
||||
// below should be canonical_url, but not provided by chainquery yet
|
||||
head += `<meta property="og:url" content="${URL}/${claim.name}:${claim.claim_id}"/>`;
|
||||
head += `<meta name="twitter:url" content="${URL}/${claim.name}:${claim.claim_id}"/>`;
|
||||
head += `<meta property="fb:app_id" content="1673146449633983" />`;
|
||||
head += `<link rel="canonical" content="${SITE_CANONICAL_URL || URL}/${claim.name}:${claim.claim_id}"/>`;
|
||||
|
||||
if (mediaType && (mediaType.startsWith('video/') || mediaType.startsWith('audio/'))) {
|
||||
const videoUrl = generateEmbedUrl(claim.name, claim.claim_id);
|
||||
head += `<meta property="og:video" content="${videoUrl}" />`;
|
||||
head += `<meta property="og:video:secure_url" content="${videoUrl}" />`;
|
||||
head += `<meta property="og:video:type" content="${mediaType}" />`;
|
||||
if (channel) {
|
||||
head += `<meta name="og:video:series" content="${channel}"/>`;
|
||||
}
|
||||
head += `<meta name="twitter:card" content="player"/>`;
|
||||
head += `<meta name="twitter:player" content="${videoUrl}" />`;
|
||||
if (releaseTime) {
|
||||
var release = new Date(releaseTime * 1000).toISOString();
|
||||
head += `<meta property="og:video:release_date" content="${release}"/>`;
|
||||
}
|
||||
if (mediaDuration) {
|
||||
head += `<meta property="og:video:duration" content="${mediaDuration}"/>`;
|
||||
}
|
||||
if (media && media.width && media.height) {
|
||||
head += `<meta property="og:video:width" content="${media.width}"/>`;
|
||||
head += `<meta property="og:video:height" content="${media.height}"/>`;
|
||||
head += `<meta name="twitter:player:width" content="${media.width}">`;
|
||||
head += `<meta name="twitter:player:height" content="${media.height}">`;
|
||||
}
|
||||
} else {
|
||||
head += `<meta name="twitter:card" content="summary_large_image"/>`;
|
||||
}
|
||||
|
||||
return head;
|
||||
}
|
||||
|
||||
function buildGoogleVideoMetadata(uri, claim) {
|
||||
const { claimName } = parseURI(uri);
|
||||
const { meta, value } = claim;
|
||||
const media = value && value.video;
|
||||
const source = value && value.source;
|
||||
const thumbnail = value && value.thumbnail && value.thumbnail.url;
|
||||
const mediaType = source && source.media_type;
|
||||
const mediaDuration = media && media.duration;
|
||||
const claimTitle = escapeHtmlProperty((value && value.title) || claimName);
|
||||
const releaseTime = (value && value.release_time) || (meta && meta.creation_timestamp) || 0;
|
||||
|
||||
const claimDescription =
|
||||
value && value.description && value.description.length > 0
|
||||
? escapeHtmlProperty(truncateDescription(value.description))
|
||||
: `View ${claimTitle} on ${SITE_NAME}`;
|
||||
|
||||
if (!mediaType || !mediaType.startsWith('video/')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const claimThumbnail = escapeHtmlProperty(thumbnail) || OG_IMAGE_URL || `${URL}/public/v2-og.png`;
|
||||
|
||||
// https://developers.google.com/search/docs/data-types/video
|
||||
const googleVideoMetadata = {
|
||||
// --- Must ---
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'VideoObject',
|
||||
name: `${claimTitle}`,
|
||||
description: `${removeMd(claimDescription)}`,
|
||||
thumbnailUrl: `${claimThumbnail}`,
|
||||
uploadDate: `${new Date(releaseTime * 1000).toISOString()}`,
|
||||
// --- Recommended ---
|
||||
duration: mediaDuration ? moment.duration(mediaDuration * 1000).toISOString() : undefined,
|
||||
contentUrl: generateDirectUrl(claim.name, claim.claim_id),
|
||||
embedUrl: generateEmbedUrl(claim.name, claim.claim_id),
|
||||
};
|
||||
|
||||
if (
|
||||
!googleVideoMetadata.description.replace(/\s/g, '').length ||
|
||||
googleVideoMetadata.thumbnailUrl.startsWith('data:image') ||
|
||||
!googleVideoMetadata.thumbnailUrl.startsWith('http')
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
'<script type="application/ld+json">\n' + JSON.stringify(googleVideoMetadata, null, ' ') + '\n' + '</script>\n'
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveClaimOrRedirect(ctx, url, ignoreRedirect = false) {
|
||||
let claim;
|
||||
try {
|
||||
const response = await Lbry.resolve({ urls: [url] });
|
||||
if (response && response[url] && !response[url].error) {
|
||||
claim = response && response[url];
|
||||
const isRepost = claim.reposted_claim && claim.reposted_claim.name && claim.reposted_claim.claim_id;
|
||||
if (isRepost && !ignoreRedirect) {
|
||||
ctx.redirect(`/${claim.reposted_claim.name}:${claim.reposted_claim.claim_id}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return claim;
|
||||
}
|
||||
|
||||
let html;
|
||||
async function getHtml(ctx) {
|
||||
if (!html) {
|
||||
html = fs.readFileSync(path.join(__dirname, '/../dist/index.html'), 'utf8');
|
||||
}
|
||||
|
||||
const requestPath = decodeURIComponent(ctx.path);
|
||||
if (requestPath.length === 0) {
|
||||
const ogMetadata = buildBasicOgMetadata();
|
||||
return insertToHead(html, ogMetadata);
|
||||
}
|
||||
|
||||
const invitePath = `/$/${PAGES.INVITE}/`;
|
||||
const embedPath = `/$/${PAGES.EMBED}/`;
|
||||
|
||||
if (requestPath.includes(invitePath)) {
|
||||
try {
|
||||
const inviteChannel = requestPath.slice(invitePath.length);
|
||||
const inviteChannelUrl = normalizeClaimUrl(inviteChannel);
|
||||
const claim = await resolveClaimOrRedirect(ctx, inviteChannelUrl);
|
||||
const invitePageMetadata = buildClaimOgMetadata(inviteChannelUrl, claim, {
|
||||
title: `Join ${claim.name} on ${SITE_NAME}`,
|
||||
description: `Join ${claim.name} on ${SITE_NAME}, a content wonderland owned by everyone (and no one).`,
|
||||
});
|
||||
|
||||
return insertToHead(html, invitePageMetadata);
|
||||
} catch (e) {
|
||||
// Something about the invite channel is messed up
|
||||
// Enter generic invite metadata
|
||||
const invitePageMetadata = buildOgMetadata({
|
||||
title: `Join a friend on ${SITE_NAME}`,
|
||||
description: `Join a friend on ${SITE_NAME}, a content wonderland owned by everyone (and no one).`,
|
||||
});
|
||||
return insertToHead(html, invitePageMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
if (requestPath.includes(embedPath)) {
|
||||
const claimUri = normalizeClaimUrl(requestPath.replace(embedPath, '').replace('/', '#'));
|
||||
const claim = await resolveClaimOrRedirect(ctx, claimUri, true);
|
||||
|
||||
if (claim) {
|
||||
const ogMetadata = buildClaimOgMetadata(claimUri, claim);
|
||||
const googleVideoMetadata = buildGoogleVideoMetadata(claimUri, claim);
|
||||
return insertToHead(html, ogMetadata.concat('\n', googleVideoMetadata));
|
||||
}
|
||||
|
||||
return insertToHead(html);
|
||||
}
|
||||
|
||||
const categoryMeta = getCategoryMeta(requestPath);
|
||||
if (categoryMeta) {
|
||||
const categoryPageMetadata = buildOgMetadata({
|
||||
title: categoryMeta.title,
|
||||
description: categoryMeta.description,
|
||||
image: categoryMeta.image,
|
||||
path: requestPath,
|
||||
});
|
||||
return insertToHead(html, categoryPageMetadata);
|
||||
}
|
||||
|
||||
if (!requestPath.includes('$')) {
|
||||
const claimUri = normalizeClaimUrl(requestPath.slice(1));
|
||||
const claim = await resolveClaimOrRedirect(ctx, claimUri);
|
||||
|
||||
if (claim) {
|
||||
const ogMetadata = buildClaimOgMetadata(claimUri, claim);
|
||||
const googleVideoMetadata = buildGoogleVideoMetadata(claimUri, claim);
|
||||
return insertToHead(html, ogMetadata.concat('\n', googleVideoMetadata));
|
||||
}
|
||||
}
|
||||
|
||||
const ogMetadataAndPWA = buildHead();
|
||||
return insertToHead(html, ogMetadataAndPWA);
|
||||
}
|
||||
|
||||
module.exports = { insertToHead, buildHead, getHtml };
|
|
@ -1,343 +0,0 @@
|
|||
// Disabled flow in this copy. This copy is for uncompiled web server ES5 require()s.
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const channelNameMinLength = 1;
|
||||
const claimIdMaxLength = 40;
|
||||
|
||||
// see https://spec.lbry.com/#urls
|
||||
const regexInvalidURI = /[ =&#:$@%?;/\\"<>%{}|^~[\]`\u{0000}-\u{0008}\u{000b}-\u{000c}\u{000e}-\u{001F}\u{D800}-\u{DFFF}\u{FFFE}-\u{FFFF}]/u;
|
||||
// const regexAddress = /^(b|r)(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/;
|
||||
const regexPartProtocol = '^((?:lbry://)?)';
|
||||
const regexPartStreamOrChannelName = '([^:$#/]*)';
|
||||
const regexPartModifierSeparator = '([:$#]?)([^/]*)';
|
||||
const queryStringBreaker = '^([\\S]+)([?][\\S]*)';
|
||||
const separateQuerystring = new RegExp(queryStringBreaker);
|
||||
|
||||
const MOD_SEQUENCE_SEPARATOR = '*';
|
||||
const MOD_CLAIM_ID_SEPARATOR_OLD = '#';
|
||||
const MOD_CLAIM_ID_SEPARATOR = ':';
|
||||
const MOD_BID_POSITION_SEPARATOR = '$';
|
||||
|
||||
/**
|
||||
* Parses a LBRY name into its component parts. Throws errors with user-friendly
|
||||
* messages for invalid names.
|
||||
*
|
||||
* Returns a dictionary with keys:
|
||||
* - path (string)
|
||||
* - isChannel (boolean)
|
||||
* - streamName (string, if present)
|
||||
* - streamClaimId (string, if present)
|
||||
* - channelName (string, if present)
|
||||
* - channelClaimId (string, if present)
|
||||
* - primaryClaimSequence (int, if present)
|
||||
* - secondaryClaimSequence (int, if present)
|
||||
* - primaryBidPosition (int, if present)
|
||||
* - secondaryBidPosition (int, if present)
|
||||
*/
|
||||
|
||||
function parseURI(url, requireProto = false) {
|
||||
// Break into components. Empty sub-matches are converted to null
|
||||
|
||||
const componentsRegex = new RegExp(
|
||||
regexPartProtocol + // protocol
|
||||
regexPartStreamOrChannelName + // stream or channel name (stops at the first separator or end)
|
||||
regexPartModifierSeparator + // modifier separator, modifier (stops at the first path separator or end)
|
||||
'(/?)' + // path separator, there should only be one (optional) slash to separate the stream and channel parts
|
||||
regexPartStreamOrChannelName +
|
||||
regexPartModifierSeparator
|
||||
);
|
||||
// chop off the querystring first
|
||||
let QSStrippedURL, qs;
|
||||
const qsRegexResult = separateQuerystring.exec(url);
|
||||
if (qsRegexResult) {
|
||||
[QSStrippedURL, qs] = qsRegexResult.slice(1).map((match) => match || null);
|
||||
}
|
||||
|
||||
const cleanURL = QSStrippedURL || url;
|
||||
const regexMatch = componentsRegex.exec(cleanURL) || [];
|
||||
const [proto, ...rest] = regexMatch.slice(1).map((match) => match || null);
|
||||
const path = rest.join('');
|
||||
const [
|
||||
streamNameOrChannelName,
|
||||
primaryModSeparator,
|
||||
primaryModValue,
|
||||
pathSep, // eslint-disable-line no-unused-vars
|
||||
possibleStreamName,
|
||||
secondaryModSeparator,
|
||||
secondaryModValue,
|
||||
] = rest;
|
||||
const searchParams = new URLSearchParams(qs || '');
|
||||
const startTime = searchParams.get('t');
|
||||
|
||||
// Validate protocol
|
||||
if (requireProto && !proto) {
|
||||
throw new Error(__('LBRY URLs must include a protocol prefix (lbry://).'));
|
||||
}
|
||||
|
||||
// Validate and process name
|
||||
if (!streamNameOrChannelName) {
|
||||
throw new Error(__('URL does not include name.'));
|
||||
}
|
||||
|
||||
rest.forEach((urlPiece) => {
|
||||
if (urlPiece && urlPiece.includes(' ')) {
|
||||
throw new Error(__('URL can not include a space'));
|
||||
}
|
||||
});
|
||||
|
||||
const includesChannel = streamNameOrChannelName.startsWith('@');
|
||||
const isChannel = streamNameOrChannelName.startsWith('@') && !possibleStreamName;
|
||||
const channelName = includesChannel && streamNameOrChannelName.slice(1);
|
||||
|
||||
if (includesChannel) {
|
||||
if (!channelName) {
|
||||
throw new Error(__('No channel name after @.'));
|
||||
}
|
||||
|
||||
if (channelName.length < channelNameMinLength) {
|
||||
throw new Error(
|
||||
__(`Channel names must be at least %channelNameMinLength% characters.`, {
|
||||
channelNameMinLength,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate and process modifier
|
||||
const [primaryClaimId, primaryClaimSequence, primaryBidPosition] = parseURIModifier(
|
||||
primaryModSeparator,
|
||||
primaryModValue
|
||||
);
|
||||
const [secondaryClaimId, secondaryClaimSequence, secondaryBidPosition] = parseURIModifier(
|
||||
secondaryModSeparator,
|
||||
secondaryModValue
|
||||
);
|
||||
const streamName = includesChannel ? possibleStreamName : streamNameOrChannelName;
|
||||
const streamClaimId = includesChannel ? secondaryClaimId : primaryClaimId;
|
||||
const channelClaimId = includesChannel && primaryClaimId;
|
||||
|
||||
return {
|
||||
isChannel,
|
||||
path,
|
||||
...(streamName ? { streamName } : {}),
|
||||
...(streamClaimId ? { streamClaimId } : {}),
|
||||
...(channelName ? { channelName } : {}),
|
||||
...(channelClaimId ? { channelClaimId } : {}),
|
||||
...(primaryClaimSequence ? { primaryClaimSequence: parseInt(primaryClaimSequence, 10) } : {}),
|
||||
...(secondaryClaimSequence ? { secondaryClaimSequence: parseInt(secondaryClaimSequence, 10) } : {}),
|
||||
...(primaryBidPosition ? { primaryBidPosition: parseInt(primaryBidPosition, 10) } : {}),
|
||||
...(secondaryBidPosition ? { secondaryBidPosition: parseInt(secondaryBidPosition, 10) } : {}),
|
||||
...(startTime ? { startTime: parseInt(startTime, 10) } : {}),
|
||||
|
||||
// The values below should not be used for new uses of parseURI
|
||||
// They will not work properly with canonical_urls
|
||||
claimName: streamNameOrChannelName,
|
||||
claimId: primaryClaimId,
|
||||
...(streamName ? { contentName: streamName } : {}),
|
||||
...(qs ? { queryString: qs } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function parseURIModifier(modSeperator, modValue) {
|
||||
let claimId;
|
||||
let claimSequence;
|
||||
let bidPosition;
|
||||
|
||||
if (modSeperator) {
|
||||
if (!modValue) {
|
||||
throw new Error(__(`No modifier provided after separator %modSeperator%.`, { modSeperator }));
|
||||
}
|
||||
|
||||
if (modSeperator === MOD_CLAIM_ID_SEPARATOR || MOD_CLAIM_ID_SEPARATOR_OLD) {
|
||||
claimId = modValue;
|
||||
} else if (modSeperator === MOD_SEQUENCE_SEPARATOR) {
|
||||
claimSequence = modValue;
|
||||
} else if (modSeperator === MOD_BID_POSITION_SEPARATOR) {
|
||||
bidPosition = modValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (claimId && (claimId.length > claimIdMaxLength || !claimId.match(/^[0-9a-f]+$/))) {
|
||||
throw new Error(__(`Invalid claim ID %claimId%.`, { claimId }));
|
||||
}
|
||||
|
||||
if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) {
|
||||
throw new Error(__('Claim sequence must be a number.'));
|
||||
}
|
||||
|
||||
if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) {
|
||||
throw new Error(__('Bid position must be a number.'));
|
||||
}
|
||||
|
||||
return [claimId, claimSequence, bidPosition];
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an object in the same format returned by parse() and builds a URI.
|
||||
*
|
||||
* The channelName key will accept names with or without the @ prefix.
|
||||
*/
|
||||
function buildURI(UrlObj, includeProto = true, protoDefault = 'lbry://') {
|
||||
const {
|
||||
streamName,
|
||||
streamClaimId,
|
||||
channelName,
|
||||
channelClaimId,
|
||||
primaryClaimSequence,
|
||||
primaryBidPosition,
|
||||
secondaryClaimSequence,
|
||||
secondaryBidPosition,
|
||||
startTime,
|
||||
...deprecatedParts
|
||||
} = UrlObj;
|
||||
const { claimId, claimName, contentName } = deprecatedParts;
|
||||
|
||||
if (!isProduction) {
|
||||
if (claimId) {
|
||||
console.error(__("'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead"));
|
||||
}
|
||||
if (claimName) {
|
||||
console.error(__("'claimName' should no longer be used. Use 'streamClaimName' or 'channelClaimName' instead"));
|
||||
}
|
||||
if (contentName) {
|
||||
console.error(__("'contentName' should no longer be used. Use 'streamName' instead"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!claimName && !channelName && !streamName) {
|
||||
console.error(
|
||||
__("'claimName', 'channelName', and 'streamName' are all empty. One must be present to build a url.")
|
||||
);
|
||||
}
|
||||
|
||||
const formattedChannelName = channelName && (channelName.startsWith('@') ? channelName : `@${channelName}`);
|
||||
const primaryClaimName = claimName || contentName || formattedChannelName || streamName;
|
||||
const primaryClaimId = claimId || (formattedChannelName ? channelClaimId : streamClaimId);
|
||||
const secondaryClaimName = (!claimName && contentName) || (formattedChannelName ? streamName : null);
|
||||
const secondaryClaimId = secondaryClaimName && streamClaimId;
|
||||
|
||||
return (
|
||||
(includeProto ? protoDefault : '') +
|
||||
// primaryClaimName will always exist here because we throw above if there is no "name" value passed in
|
||||
// $FlowFixMe
|
||||
primaryClaimName +
|
||||
(primaryClaimId ? `#${primaryClaimId}` : '') +
|
||||
(primaryClaimSequence ? `:${primaryClaimSequence}` : '') +
|
||||
(primaryBidPosition ? `${primaryBidPosition}` : '') +
|
||||
(secondaryClaimName ? `/${secondaryClaimName}` : '') +
|
||||
(secondaryClaimId ? `#${secondaryClaimId}` : '') +
|
||||
(secondaryClaimSequence ? `:${secondaryClaimSequence}` : '') +
|
||||
(secondaryBidPosition ? `${secondaryBidPosition}` : '') +
|
||||
(startTime ? `?t=${startTime}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
/* Takes a parseable LBRY URL and converts it to standard, canonical format */
|
||||
function normalizeURI(URL) {
|
||||
const {
|
||||
streamName,
|
||||
streamClaimId,
|
||||
channelName,
|
||||
channelClaimId,
|
||||
primaryClaimSequence,
|
||||
primaryBidPosition,
|
||||
secondaryClaimSequence,
|
||||
secondaryBidPosition,
|
||||
startTime,
|
||||
} = parseURI(URL);
|
||||
|
||||
return buildURI({
|
||||
streamName,
|
||||
streamClaimId,
|
||||
channelName,
|
||||
channelClaimId,
|
||||
primaryClaimSequence,
|
||||
primaryBidPosition,
|
||||
secondaryClaimSequence,
|
||||
secondaryBidPosition,
|
||||
startTime,
|
||||
});
|
||||
}
|
||||
|
||||
function isURIValid(URL) {
|
||||
try {
|
||||
parseURI(normalizeURI(URL));
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isNameValid(claimName) {
|
||||
return !regexInvalidURI.test(claimName);
|
||||
}
|
||||
|
||||
function isURIClaimable(URL) {
|
||||
let parts;
|
||||
try {
|
||||
parts = parseURI(normalizeURI(URL));
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parts && parts.streamName && !parts.streamClaimId && !parts.isChannel;
|
||||
}
|
||||
|
||||
function convertToShareLink(URL) {
|
||||
const {
|
||||
streamName,
|
||||
streamClaimId,
|
||||
channelName,
|
||||
channelClaimId,
|
||||
primaryBidPosition,
|
||||
primaryClaimSequence,
|
||||
secondaryBidPosition,
|
||||
secondaryClaimSequence,
|
||||
} = parseURI(URL);
|
||||
return buildURI(
|
||||
{
|
||||
streamName,
|
||||
streamClaimId,
|
||||
channelName,
|
||||
channelClaimId,
|
||||
primaryBidPosition,
|
||||
primaryClaimSequence,
|
||||
secondaryBidPosition,
|
||||
secondaryClaimSequence,
|
||||
},
|
||||
true,
|
||||
'https://open.lbry.com/'
|
||||
);
|
||||
}
|
||||
|
||||
function splitBySeparator(uri) {
|
||||
const protocolLength = 7;
|
||||
return uri.startsWith('lbry://') ? uri.slice(protocolLength).split(/[#:*]/) : uri.split(/#:\*\$/);
|
||||
}
|
||||
|
||||
function isURIEqual(uriA, uriB) {
|
||||
const parseA = parseURI(normalizeURI(uriA));
|
||||
const parseB = parseURI(normalizeURI(uriB));
|
||||
if (parseA.isChannel) {
|
||||
if (parseB.isChannel && parseA.channelClaimId === parseB.channelClaimId) {
|
||||
return true;
|
||||
}
|
||||
} else if (parseA.streamClaimId === parseB.streamClaimId) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseURI,
|
||||
buildURI,
|
||||
normalizeURI,
|
||||
isURIValid,
|
||||
isURIEqual,
|
||||
isNameValid,
|
||||
isURIClaimable,
|
||||
splitBySeparator,
|
||||
convertToShareLink,
|
||||
};
|
|
@ -1,12 +0,0 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let robots;
|
||||
async function getRobots(ctx) {
|
||||
if (!robots) {
|
||||
robots = fs.readFileSync(path.join(__dirname, '/../dist/public/robots.txt'), 'utf8');
|
||||
}
|
||||
return robots;
|
||||
}
|
||||
|
||||
module.exports = { getRobots };
|
|
@ -1,76 +0,0 @@
|
|||
const { getHtml } = require('./html');
|
||||
const { getRss } = require('./rss');
|
||||
const { getHomepageJSON } = require('./getHomepageJSON');
|
||||
const { generateStreamUrl } = require('../../ui/util/web');
|
||||
const fetch = require('node-fetch');
|
||||
const Router = require('@koa/router');
|
||||
const { CUSTOM_HOMEPAGE } = require('../../config.js');
|
||||
|
||||
// So any code from 'lbry-redux'/'lbryinc' that uses `fetch` can be run on the server
|
||||
global.fetch = fetch;
|
||||
|
||||
const router = new Router();
|
||||
|
||||
function getStreamUrl(ctx) {
|
||||
const { claimName, claimId } = ctx.params;
|
||||
|
||||
const streamUrl = generateStreamUrl(claimName, claimId);
|
||||
return streamUrl;
|
||||
}
|
||||
|
||||
const rssMiddleware = async (ctx) => {
|
||||
const rss = await getRss(ctx);
|
||||
if (rss.startsWith('<?xml')) {
|
||||
ctx.set('Content-Type', 'application/xml');
|
||||
}
|
||||
ctx.body = rss;
|
||||
};
|
||||
|
||||
router.get(`/$/api/content/v1/get`, async (ctx) => {
|
||||
if (!CUSTOM_HOMEPAGE) {
|
||||
ctx.status = 404;
|
||||
ctx.body = {
|
||||
message: 'Not Found',
|
||||
};
|
||||
} else {
|
||||
let content;
|
||||
try {
|
||||
content = getHomepageJSON();
|
||||
ctx.set('Content-Type', 'application/json');
|
||||
ctx.body = {
|
||||
status: 'success',
|
||||
data: content,
|
||||
};
|
||||
} catch (err) {
|
||||
ctx.status = err.statusCode || err.status || 500;
|
||||
ctx.body = {
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.get(`/$/download/:claimName/:claimId`, async (ctx) => {
|
||||
const streamUrl = getStreamUrl(ctx);
|
||||
const downloadUrl = `${streamUrl}?download=1`;
|
||||
ctx.redirect(downloadUrl);
|
||||
});
|
||||
|
||||
router.get(`/$/stream/:claimName/:claimId`, async (ctx) => {
|
||||
const streamUrl = getStreamUrl(ctx);
|
||||
ctx.redirect(streamUrl);
|
||||
});
|
||||
|
||||
router.get(`/$/activate`, async (ctx) => {
|
||||
ctx.redirect(`https://sso.odysee.com/auth/realms/Users/device`);
|
||||
});
|
||||
|
||||
router.get(`/$/rss/:claimName/:claimId`, rssMiddleware);
|
||||
router.get(`/$/rss/:claimName::claimId`, rssMiddleware);
|
||||
|
||||
router.get('*', async (ctx) => {
|
||||
const html = await getHtml(ctx);
|
||||
ctx.body = html;
|
||||
});
|
||||
|
||||
module.exports = router;
|
298
web/src/rss.js
298
web/src/rss.js
|
@ -1,298 +0,0 @@
|
|||
const { generateStreamUrl } = require('../../ui/util/web');
|
||||
const { URL, SITE_NAME, LBRY_WEB_API } = require('../../config.js');
|
||||
const { lbryProxy: Lbry } = require('../lbry');
|
||||
const Rss = require('rss');
|
||||
const Mime = require('mime-types');
|
||||
|
||||
const SDK_API_PATH = `${LBRY_WEB_API}/api/v1`;
|
||||
const proxyURL = `${SDK_API_PATH}/proxy`;
|
||||
Lbry.setDaemonConnectionString(proxyURL);
|
||||
|
||||
const NUM_ENTRIES = 500;
|
||||
|
||||
// ****************************************************************************
|
||||
// Fetch claim info
|
||||
// ****************************************************************************
|
||||
|
||||
async function doClaimSearch(options) {
|
||||
let results;
|
||||
try {
|
||||
results = await Lbry.claim_search(options);
|
||||
} catch {}
|
||||
return results ? results.items : undefined;
|
||||
}
|
||||
|
||||
async function getChannelClaim(name, claimId) {
|
||||
let claim;
|
||||
let error;
|
||||
|
||||
try {
|
||||
const url = `lbry://${name}#${claimId}`;
|
||||
const response = await Lbry.resolve({ urls: [url] });
|
||||
if (response && response[url] && !response[url].error) {
|
||||
claim = response && response[url];
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (!claim) {
|
||||
error = 'The RSS URL is invalid or is not associated with any channel.';
|
||||
}
|
||||
|
||||
return { claim, error };
|
||||
}
|
||||
|
||||
async function getClaimsFromChannel(claimId, count) {
|
||||
const options = {
|
||||
channel_ids: [claimId],
|
||||
page_size: count,
|
||||
has_source: true,
|
||||
claim_type: 'stream',
|
||||
order_by: ['release_time'],
|
||||
no_totals: true,
|
||||
};
|
||||
|
||||
return await doClaimSearch(options);
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Helpers
|
||||
// ****************************************************************************
|
||||
|
||||
function encodeWithSpecialCharEncode(string) {
|
||||
// encodeURIComponent doesn't encode `'` and others
|
||||
// which other services may not like
|
||||
return encodeURIComponent(string).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29');
|
||||
}
|
||||
|
||||
const generateEnclosureForClaimContent = (claim) => {
|
||||
const value = claim.value;
|
||||
if (!value || !value.stream_type) {
|
||||
return undefined;
|
||||
}
|
||||
const fileExt = value.source && value.source.media_type && '.' + Mime.extension(value.source.media_type);
|
||||
|
||||
switch (value.stream_type) {
|
||||
case 'video':
|
||||
return {
|
||||
url: generateStreamUrl(claim.name, claim.claim_id) + (fileExt || '.mp4'),
|
||||
type: (value.source && value.source.media_type) || 'video/mp4',
|
||||
size: (value.source && value.source.size) || 0, // Per spec, 0 is a valid fallback.
|
||||
};
|
||||
|
||||
case 'audio':
|
||||
return {
|
||||
url: generateStreamUrl(claim.name, claim.claim_id) + ((fileExt === '.mpga' ? '.mp3' : fileExt) || '.mp3'),
|
||||
type: (value.source && value.source.media_type) || 'audio/mpeg',
|
||||
size: (value.source && value.source.size) || 0, // Per spec, 0 is a valid fallback.
|
||||
};
|
||||
case 'image':
|
||||
return {
|
||||
url: generateStreamUrl(claim.name, claim.claim_id) + (fileExt || '.jpeg'),
|
||||
type: (value.source && value.source.media_type) || 'image/jpeg',
|
||||
size: (value.source && value.source.size) || 0, // Per spec, 0 is a valid fallback.
|
||||
};
|
||||
case 'document':
|
||||
case 'software':
|
||||
return {
|
||||
url: generateStreamUrl(claim.name, claim.claim_id),
|
||||
type: (value.source && value.source.media_type) || undefined,
|
||||
size: (value.source && value.source.size) || 0, // Per spec, 0 is a valid fallback.
|
||||
};
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const getLanguageValue = (claim) => {
|
||||
if (claim && claim.value && claim.value.languages && claim.value.languages.length > 0) {
|
||||
return claim.value.languages[0];
|
||||
}
|
||||
return 'en';
|
||||
};
|
||||
|
||||
const replaceLineFeeds = (str) => str.replace(/(?:\r\n|\r|\n)/g, '<br />');
|
||||
|
||||
const isEmailRoughlyValid = (email) => /^\S+@\S+$/.test(email);
|
||||
|
||||
/**
|
||||
* 'itunes:owner' is required by castfeedvalidator (w3c allows omission), and
|
||||
* both name and email must be defined. The email must also be a "valid" one.
|
||||
*
|
||||
* Use a fallback email when the creator did not specify one. The email will not
|
||||
* be shown to the user; it is just used for administrative purposes.
|
||||
*
|
||||
* @param claim
|
||||
* @returns any
|
||||
*/
|
||||
const generateItunesOwnerElement = (claim) => {
|
||||
let email = 'no-reply@odysee.com';
|
||||
let name = claim && (claim.value && claim.value.title ? claim.value.title : claim.name);
|
||||
|
||||
if (claim && claim.value) {
|
||||
if (isEmailRoughlyValid(claim.value.email)) {
|
||||
email = claim.value.email;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'itunes:owner': [{ 'itunes:name': name }, { 'itunes:email': email }],
|
||||
};
|
||||
};
|
||||
|
||||
const generateItunesExplicitElement = (claim) => {
|
||||
const tags = (claim && claim.value && claim.value.tags) || [];
|
||||
return { 'itunes:explicit': tags.includes('mature') ? 'yes' : 'no' };
|
||||
};
|
||||
|
||||
const getItunesCategory = (claim) => {
|
||||
const itunesCategories = [
|
||||
'Arts',
|
||||
'Business',
|
||||
'Comedy',
|
||||
'Education',
|
||||
'Fiction',
|
||||
'Government',
|
||||
'History',
|
||||
'Health & Fitness',
|
||||
'Kids & Family',
|
||||
'Leisure',
|
||||
'Music',
|
||||
'News',
|
||||
'Religion & Spirituality',
|
||||
'Science',
|
||||
'Society & Culture',
|
||||
'Sports',
|
||||
'Technology',
|
||||
'True Crime',
|
||||
'TV & Film',
|
||||
];
|
||||
|
||||
const tags = (claim && claim.value && claim.value.tags) || [];
|
||||
|
||||
for (let i = 0; i < itunesCategories.length; ++i) {
|
||||
const itunesCategory = itunesCategories[i];
|
||||
if (tags.includes(itunesCategory.toLowerCase())) {
|
||||
// "Note: Although you can specify more than one category and subcategory
|
||||
// in your RSS feed, Apple Podcasts only recognizes the first category and
|
||||
// subcategory."
|
||||
// --> The only parse the first found tag.
|
||||
return itunesCategory.replace('&', '&');
|
||||
}
|
||||
}
|
||||
|
||||
// itunes will not accept any other categories, and the element is required
|
||||
// to pass castfeedvalidator. So, fallback to 'Leisure' (closes to "General")
|
||||
// if the creator did not specify a tag.
|
||||
return 'Leisure';
|
||||
};
|
||||
|
||||
const generateItunesDurationElement = (claim) => {
|
||||
let duration;
|
||||
if (claim && claim.value) {
|
||||
if (claim.value.video) {
|
||||
duration = claim.value.video.duration;
|
||||
} else if (claim.value.audio) {
|
||||
duration = claim.value.audio.duration;
|
||||
}
|
||||
}
|
||||
|
||||
if (duration) {
|
||||
return { 'itunes:duration': `${duration}` };
|
||||
}
|
||||
};
|
||||
|
||||
const generateItunesImageElement = (claim) => {
|
||||
const thumbnailUrl = (claim && claim.value && claim.value.thumbnail && claim.value.thumbnail.url) || '';
|
||||
if (thumbnailUrl) {
|
||||
return {
|
||||
'itunes:image': { _attr: { href: thumbnailUrl } },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getFormattedDescription = (claim) => {
|
||||
return replaceLineFeeds((claim && claim.value && claim.value.description) || '');
|
||||
};
|
||||
|
||||
// ****************************************************************************
|
||||
// Generate
|
||||
// ****************************************************************************
|
||||
|
||||
function generateFeed(feedLink, channelClaim, claimsInChannel) {
|
||||
// --- Channel ---
|
||||
let channelTitle = (channelClaim.value && channelClaim.value.title) || channelClaim.name;
|
||||
const feed = new Rss({
|
||||
title: channelTitle + ' on ' + SITE_NAME,
|
||||
description: getFormattedDescription(channelClaim),
|
||||
feed_url: feedLink,
|
||||
site_url: (channelClaim.value && channelClaim.value.website_url) || URL,
|
||||
image_url: (channelClaim.value && channelClaim.value.thumbnail && channelClaim.value.thumbnail.url) || undefined,
|
||||
language: getLanguageValue(channelClaim),
|
||||
custom_namespaces: { itunes: 'http://www.itunes.com/dtds/podcast-1.0.dtd' },
|
||||
custom_elements: [
|
||||
{ 'itunes:author': channelTitle },
|
||||
{
|
||||
'itunes:category': [
|
||||
{
|
||||
_attr: {
|
||||
text: getItunesCategory(channelClaim),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
generateItunesImageElement(channelClaim),
|
||||
generateItunesOwnerElement(channelClaim),
|
||||
generateItunesExplicitElement(channelClaim),
|
||||
],
|
||||
});
|
||||
|
||||
// --- Content ---
|
||||
claimsInChannel.forEach((c) => {
|
||||
const title = (c.value && c.value.title) || c.name;
|
||||
const thumbnailUrl = (c.value && c.value.thumbnail && c.value.thumbnail.url) || '';
|
||||
const thumbnailHtml = thumbnailUrl
|
||||
? `<p><img src="${thumbnailUrl}" width="480" alt="thumbnail" title="${title}" /></p>`
|
||||
: '';
|
||||
const description = thumbnailHtml + getFormattedDescription(c);
|
||||
|
||||
const url = `${URL}/${encodeWithSpecialCharEncode(c.name)}:${c.claim_id}`;
|
||||
const date = c.release_time ? c.release_time * 1000 : c.meta && c.meta.creation_timestamp * 1000;
|
||||
|
||||
feed.item({
|
||||
title: title,
|
||||
description: description,
|
||||
url: url,
|
||||
guid: undefined, // defaults to 'url'
|
||||
author: undefined, // defaults feed author property
|
||||
date: new Date(date),
|
||||
enclosure: generateEnclosureForClaimContent(c),
|
||||
custom_elements: [
|
||||
{ 'itunes:title': title },
|
||||
{ 'itunes:author': channelTitle },
|
||||
generateItunesImageElement(c),
|
||||
generateItunesDurationElement(c),
|
||||
generateItunesExplicitElement(c),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
return feed;
|
||||
}
|
||||
|
||||
async function getRss(ctx) {
|
||||
if (!ctx.params.claimName || !ctx.params.claimId) {
|
||||
return 'Invalid URL';
|
||||
}
|
||||
|
||||
const { claim: channelClaim, error } = await getChannelClaim(ctx.params.claimName, ctx.params.claimId);
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
const latestClaimsInChannel = await getClaimsFromChannel(channelClaim.claim_id, NUM_ENTRIES);
|
||||
const feed = generateFeed(`${URL}${ctx.request.url}`, channelClaim, latestClaimsInChannel);
|
||||
return feed.xml();
|
||||
}
|
||||
|
||||
module.exports = { getRss };
|
|
@ -1,18 +0,0 @@
|
|||
const { URL, SITE_TITLE, FAVICON } = require('../../config.js');
|
||||
const favicon = FAVICON || `${URL}/public/favicon.png`;
|
||||
function getOpenSearchXml() {
|
||||
return (
|
||||
`<ShortName>${SITE_TITLE}</ShortName>` +
|
||||
`<Description>Search ${SITE_TITLE}</Description>` +
|
||||
'<InputEncoding>UTF-8</InputEncoding>' +
|
||||
`<Image width="32" height="32" type="image/png">${favicon}</Image>` +
|
||||
`<Url type="text/html" method="get" template="${URL}/$/search?q={searchTerms}"/>` +
|
||||
`<moz:SearchForm>${URL}</moz:SearchForm>`
|
||||
);
|
||||
}
|
||||
|
||||
function insertVariableXml(fullXml, xmlToInsert) {
|
||||
return fullXml.replace(/<!-- VARIABLE_XML_BEGIN -->.*<!-- VARIABLE_XML_END -->/s, xmlToInsert);
|
||||
}
|
||||
|
||||
module.exports = { getOpenSearchXml, insertVariableXml };
|
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
Before Width: | Height: | Size: 60 KiB |
Binary file not shown.
Before Width: | Height: | Size: 24 KiB |
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"name": "Odysee",
|
||||
"short_name": "Odysee",
|
||||
"theme_color": "#fa6164",
|
||||
"background_color": "#FAFAFA",
|
||||
"display": "standalone",
|
||||
"description": "Launch your own channel | Watch and share videos",
|
||||
"scope": "/",
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/public/pwa/icon-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "/public/pwa/icon-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
},
|
||||
{
|
||||
"src": "/public/pwa/icon-180.png",
|
||||
"type": "image/png",
|
||||
"sizes": "180x180"
|
||||
}
|
||||
],
|
||||
"prefer_related_applications": false
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
// register service worker
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register('./serviceWorker.js')
|
||||
.then(function(reg) {
|
||||
// reg.scope must be '/' to allow '/' as start url
|
||||
})
|
||||
.catch(function(err) {
|
||||
// console.warn('Error whilst registering service worker', err);
|
||||
});
|
||||
}
|
||||
|
||||
// used to fetch the manifest file
|
||||
self.addEventListener('fetch', () => {});
|
|
@ -1,43 +0,0 @@
|
|||
const callable = () => {
|
||||
throw new Error('Need to fix this stub');
|
||||
};
|
||||
const { DEFAULT_LANGUAGE } = require('../../config.js');
|
||||
export const remote = {
|
||||
dialog: {
|
||||
showOpenDialog: callable,
|
||||
},
|
||||
getCurrentWindow: callable,
|
||||
app: {
|
||||
getAppPath: callable,
|
||||
getLocale: () => {
|
||||
return DEFAULT_LANGUAGE;
|
||||
},
|
||||
},
|
||||
BrowserWindow: {
|
||||
getFocusedWindow: callable,
|
||||
},
|
||||
Menu: {
|
||||
getApplicationMenu: callable,
|
||||
buildFromTemplate: () => {
|
||||
return {
|
||||
popup: () => {},
|
||||
};
|
||||
},
|
||||
},
|
||||
require: callable,
|
||||
};
|
||||
|
||||
export const clipboard = {
|
||||
readText: () => '',
|
||||
writeText: text => {
|
||||
var dummy = document.createElement('textarea');
|
||||
document.body.appendChild(dummy);
|
||||
dummy.value = text;
|
||||
dummy.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(dummy);
|
||||
},
|
||||
};
|
||||
export const ipcRenderer = {};
|
||||
|
||||
export const isDev = false;
|
|
@ -1,17 +0,0 @@
|
|||
function logWarning(method) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.error(`Called fs.${method} on lbry.tv. This should be removed.`);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
readFileSync: () => {
|
||||
logWarning('readFileSync');
|
||||
return undefined;
|
||||
},
|
||||
accessFileSync: () => {
|
||||
logWarning('accessFileSync');
|
||||
return undefined;
|
||||
},
|
||||
constants: {},
|
||||
};
|
|
@ -1,6 +0,0 @@
|
|||
// @if BRANDED_SITE='odysee'
|
||||
import './scss/odysee.scss';
|
||||
// @endif
|
||||
// @if BRANDED_SITE='lbrytv'
|
||||
import './scss/lbrytv.scss';
|
||||
// @endif
|
|
@ -1,181 +0,0 @@
|
|||
const { WEBPACK_WEB_PORT, LBRY_WEB_API, BRANDED_SITE } = require('../config.js');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const merge = require('webpack-merge');
|
||||
const baseConfig = require('../webpack.base.config.js');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const WriteFilePlugin = require('write-file-webpack-plugin');
|
||||
const { DefinePlugin, ProvidePlugin } = require('webpack');
|
||||
const SentryWebpackPlugin = require('@sentry/webpack-plugin');
|
||||
const { getJsBundleId } = require('./bundle-id.js');
|
||||
const { insertToHead, buildHead } = require('./src/html');
|
||||
const { insertVariableXml, getOpenSearchXml } = require('./src/xml');
|
||||
|
||||
const CUSTOM_ROOT = path.resolve(__dirname, '../custom/');
|
||||
const STATIC_ROOT = path.resolve(__dirname, '../static/');
|
||||
const UI_ROOT = path.resolve(__dirname, '../ui/');
|
||||
const DIST_ROOT = path.resolve(__dirname, 'dist/');
|
||||
const WEB_STATIC_ROOT = path.resolve(__dirname, 'static/');
|
||||
const WEB_PLATFORM_ROOT = __dirname;
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const hasSentryToken = process.env.SENTRY_AUTH_TOKEN !== undefined;
|
||||
const jsBundleId = getJsBundleId();
|
||||
|
||||
// copy static files to dist folder
|
||||
const copyWebpackCommands = [
|
||||
{
|
||||
from: `${STATIC_ROOT}/index-web.html`,
|
||||
to: `${DIST_ROOT}/index.html`,
|
||||
// add javascript script to index.html, generate/insert metatags
|
||||
transform(content, path) {
|
||||
return insertToHead(content.toString(), buildHead());
|
||||
},
|
||||
force: true,
|
||||
},
|
||||
{
|
||||
from: `${STATIC_ROOT}/opensearch.xml`,
|
||||
to: `${DIST_ROOT}/opensearch.xml`,
|
||||
transform(content, path) {
|
||||
return insertVariableXml(content.toString(), getOpenSearchXml());
|
||||
},
|
||||
force: true,
|
||||
},
|
||||
{
|
||||
from: `${STATIC_ROOT}/robots.txt`,
|
||||
to: `${DIST_ROOT}/robots.txt`,
|
||||
force: true,
|
||||
},
|
||||
{
|
||||
from: `${STATIC_ROOT}/img/favicon.png`,
|
||||
to: `${DIST_ROOT}/public/favicon.png`,
|
||||
force: true,
|
||||
},
|
||||
{
|
||||
from: `${STATIC_ROOT}/img/favicon-spaceman.png`,
|
||||
to: `${DIST_ROOT}/public/favicon-spaceman.png`,
|
||||
force: true,
|
||||
},
|
||||
{
|
||||
from: `${STATIC_ROOT}/img/v2-og.png`,
|
||||
to: `${DIST_ROOT}/public/v2-og.png`,
|
||||
},
|
||||
{
|
||||
from: `${STATIC_ROOT}/font/`,
|
||||
to: `${DIST_ROOT}/public/font/`,
|
||||
},
|
||||
{
|
||||
from: `${WEB_STATIC_ROOT}/pwa/`,
|
||||
to: `${DIST_ROOT}/public/pwa/`,
|
||||
},
|
||||
{
|
||||
from: `${WEB_STATIC_ROOT}/pwa/serviceWorker.js`,
|
||||
to: `${DIST_ROOT}/`,
|
||||
},
|
||||
];
|
||||
|
||||
const CUSTOM_OG_PATH = `${CUSTOM_ROOT}/v2-og.png`;
|
||||
if (fs.existsSync(CUSTOM_OG_PATH)) {
|
||||
copyWebpackCommands.push({
|
||||
from: CUSTOM_OG_PATH,
|
||||
to: `${DIST_ROOT}/public/v2-og.png`,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
// clear the dist folder of existing js files before compilation
|
||||
let regex = /^.*\.(json|js|map)$/;
|
||||
// only run on nonprod environments to avoid side effects on prod
|
||||
if (!isProduction) {
|
||||
const path = `${DIST_ROOT}/public/`;
|
||||
if (fs.existsSync(path)) {
|
||||
fs.readdirSync(path)
|
||||
.filter((f) => regex.test(f))
|
||||
.map((f) => fs.unlinkSync(path + f));
|
||||
}
|
||||
}
|
||||
|
||||
const ROBOTS_TXT_PATH = `${CUSTOM_ROOT}/robots.txt`;
|
||||
if (fs.existsSync(ROBOTS_TXT_PATH)) {
|
||||
copyWebpackCommands.push({
|
||||
from: ROBOTS_TXT_PATH,
|
||||
to: `${DIST_ROOT}/robots.txt`,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
let plugins = [
|
||||
new WriteFilePlugin(),
|
||||
new CopyWebpackPlugin(copyWebpackCommands),
|
||||
new DefinePlugin({
|
||||
IS_WEB: JSON.stringify(true),
|
||||
'process.env.SDK_API_URL': JSON.stringify(process.env.SDK_API_URL || LBRY_WEB_API),
|
||||
}),
|
||||
new ProvidePlugin({
|
||||
__: ['i18n.js', '__'],
|
||||
}),
|
||||
];
|
||||
|
||||
if (isProduction && hasSentryToken) {
|
||||
plugins.push(
|
||||
new SentryWebpackPlugin({
|
||||
include: './dist',
|
||||
ignoreFile: '.sentrycliignore',
|
||||
ignore: ['node_modules', 'webpack.config.js'],
|
||||
configFile: 'sentry.properties',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const webConfig = {
|
||||
target: 'web',
|
||||
entry: {
|
||||
[`ui-${jsBundleId}`]: '../ui/index.jsx',
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.join(__dirname, 'dist/public/'),
|
||||
publicPath: '/public/',
|
||||
chunkFilename: '[name]-[chunkhash].js',
|
||||
},
|
||||
devServer: {
|
||||
port: WEBPACK_WEB_PORT,
|
||||
contentBase: path.join(__dirname, 'dist'),
|
||||
disableHostCheck: true, // to allow debugging with ngrok
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
test: /\.jsx?$/,
|
||||
options: {
|
||||
presets: ['@babel/env', '@babel/react', '@babel/flow'],
|
||||
plugins: ['@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-object-rest-spread', '@babel/plugin-proposal-class-properties'],
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'preprocess-loader',
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
TARGET: 'web',
|
||||
BRANDED_SITE: BRANDED_SITE,
|
||||
ppOptions: {
|
||||
type: 'js',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
modules: [UI_ROOT, __dirname],
|
||||
|
||||
alias: {
|
||||
// lbryinc: '../extras/lbryinc',
|
||||
electron: `${WEB_PLATFORM_ROOT}/stubs/electron.js`,
|
||||
fs: `${WEB_PLATFORM_ROOT}/stubs/fs.js`,
|
||||
},
|
||||
},
|
||||
plugins,
|
||||
};
|
||||
|
||||
module.exports = merge(baseConfig, webConfig);
|
5692
web/yarn.lock
5692
web/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue