remove web and embed

This commit is contained in:
zeppi 2021-10-22 10:46:59 -04:00 committed by jessopb
parent b50779f1e5
commit 7d5d7d3c55
57 changed files with 65 additions and 11915 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +0,0 @@
const { v4: uuid } = require('uuid');
const jsBundleId = uuid();
function getJsBundleId() {
return jsBundleId;
}
module.exports = { getJsBundleId };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,2 +0,0 @@
import PageCode2257 from './view';
export default PageCode2257;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,2 +0,0 @@
:root {
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
: '';
}
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 };

View file

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

View file

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

View file

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

View file

@ -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('&', '&amp;');
}
}
// 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 };

View file

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

View file

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

View file

@ -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', () => {});

View file

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

View file

@ -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: {},
};

View file

@ -1,6 +0,0 @@
// @if BRANDED_SITE='odysee'
import './scss/odysee.scss';
// @endif
// @if BRANDED_SITE='lbrytv'
import './scss/lbrytv.scss';
// @endif

View file

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

File diff suppressed because it is too large Load diff