Tile Grid Revamp (#1502)

* Save

* Save

* Add pulse

* Adjust footer ad

* Adjust tile ad

* Adjust tile ad hover

* Fix premium badge alignment in tile grid

* Adjust livestream icon

* Adjust livestream icon

* Save scheduled livestreasm & tile ad

* Fix scheduled callback

* Fix playlist icon size on file page

* Fix grid distortion in 3 & 4 column layout

* -

* Fix grid on category & channel page

* Fix Premium Plus Grid

* Add custom tile for adblockers

* Reset env

* Remove collapsed tiles

* Remove setLoaded on scheduled livestreams page

* -

* Make isHidden optional

* Remove px

* Review adjustments

* Inject Premium+ ads

* Fix injection

* Fix injection when using the last tile

* Fix injection when using the last tile

* Enable stripe dev

* Create PremiumPlusTile component and add list view design

* Create PremiumPlusTile component and add list view design

* Adjust ads in list view

* Remove setState from render loop

* Clean code

* Fix livestream margin in list view

* Rewrite & tune some logic - Homepage & Channel page

* Clean details...

* Clean details...

* Requested review changes

Signed-off-by: Raphael Wickihalder <raphael.wickihalder@odysee.com>

* Requested review changes

Signed-off-by: Raphael Wickihalder <raphael.wickihalder@odysee.com>
This commit is contained in:
Rave | 図書館猫 2022-05-18 13:16:35 +02:00 committed by GitHub
parent 96e704e5d9
commit ffdb5abf63
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 439 additions and 58 deletions

1
flow-typed/gui.js vendored
View file

@ -2,5 +2,4 @@
declare type ListInjectedItem = {
node: Node | (index: number, lastVisibleIndex: ?number, pageSize: ?number) => Node,
index?: number,
replace?: boolean
};

View file

@ -7,11 +7,13 @@ import {
makeSelectTotalPagesInChannelSearch,
selectClaimForUri,
} from 'redux/selectors/claims';
import { selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
import { doResolveUris } from 'redux/actions/claims';
import * as SETTINGS from 'constants/settings';
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { withRouter } from 'react-router';
import { selectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
import { selectAdBlockerFound } from 'redux/selectors/app';
import { doFetchChannelLiveStatus } from 'redux/actions/livestream';
import { selectActiveLivestreamForChannel, selectActiveLivestreamInitialized } from 'redux/selectors/livestream';
import { getChannelIdFromClaim } from 'util/claim';
@ -35,6 +37,8 @@ const select = (state, props) => {
tileLayout: selectClientSetting(state, SETTINGS.TILE_LAYOUT),
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, channelClaimId),
activeLivestreamInitialized: selectActiveLivestreamInitialized(state),
adBlockerFound: selectAdBlockerFound(state),
hasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
};
};

View file

@ -15,6 +15,7 @@ import ScheduledStreams from 'component/scheduledStreams';
import { SearchResults } from './internal/searchResults';
import useFetchLiveStatus from 'effects/use-fetch-live';
import { useIsLargeScreen } from 'effects/use-screensize';
import PremiumPlusTile from 'component/premiumPlusTile';
const TYPES_TO_ALLOW_FILTER = ['stream', 'repost'];
@ -41,6 +42,8 @@ type Props = {
doFetchChannelLiveStatus: (string) => void,
activeLivestreamForChannel: any,
activeLivestreamInitialized: boolean,
adBlockerFound: ?boolean,
hasPremiumPlus: ?boolean,
};
function ChannelContent(props: Props) {
@ -62,6 +65,8 @@ function ChannelContent(props: Props) {
doFetchChannelLiveStatus,
activeLivestreamForChannel,
activeLivestreamInitialized,
adBlockerFound,
hasPremiumPlus,
} = props;
// const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
@ -159,7 +164,15 @@ function ChannelContent(props: Props) {
defaultOrderBy={CS.ORDER_BY_NEW}
pageSize={dynamicPageSize}
infiniteScroll={defaultInfiniteScroll}
injectedItem={{ node: <Ads type="video" tileLayout={tileLayout} small /> }}
injectedItem={
!hasPremiumPlus && {
node: adBlockerFound ? (
<PremiumPlusTile tileLayout={tileLayout} />
) : (
<Ads small type="video" tileLayout />
),
}
}
meta={
showFilters && (
<Form onSubmit={() => {}} className="wunderbar--inline">

View file

@ -27,7 +27,7 @@ export default function CollectionAddButton(props: Props) {
button={!fileAction ? 'alt' : undefined}
className={classnames({ 'button--file-action': fileAction })}
icon={fileAction ? (!isSaved ? ICONS.ADD : ICONS.STACK) : ICONS.LIBRARY}
iconSize={fileAction ? 22 : undefined}
iconSize={fileAction ? 16 : undefined}
label={uri ? (!isSaved ? __('Save') : __('Saved')) : __('New List')}
requiresAuth
onClick={(e) => {

View file

@ -1,7 +1,7 @@
// @flow
import { MAIN_CLASS } from 'constants/classnames';
import type { Node } from 'react';
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import classnames from 'classnames';
import ClaimPreview from 'component/claimPreview';
import Spinner from 'component/spinner';
@ -61,6 +61,7 @@ type Props = {
unavailableUris?: Array<string>,
showMemberBadge?: boolean,
inWatchHistory?: boolean,
onHidden: string,
};
export default function ClaimList(props: Props) {
@ -102,9 +103,11 @@ export default function ClaimList(props: Props) {
unavailableUris,
showMemberBadge,
inWatchHistory,
onHidden,
} = props;
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
const [uriBuffer, setUriBuffer] = useState([]);
// Resolve the index for injectedItem, if provided; else injectedIndex will be 'undefined'.
const listRef = React.useRef();
@ -154,12 +157,6 @@ export default function ClaimList(props: Props) {
return claim.name.length === 24 && !claim.name.includes(' ') && claim.value.author === 'Spee.ch';
}, []);
// @if process.env.NODE_ENV!='production'
if (injectedItem && injectedItem.replace) {
throw new Error('claimList: "injectedItem.replace" is not implemented yet');
}
// @endif
useEffect(() => {
const handleScroll = debounce((e) => {
if (page && pageSize && onScrollBottom) {
@ -208,6 +205,19 @@ export default function ClaimList(props: Props) {
/>
);
React.useEffect(() => {
tileUris.forEach((uri, index) => {
if (uri) {
const inj = getInjectedItem(index);
if (inj) {
if (uriBuffer.indexOf(index) === -1) {
setUriBuffer([index]);
}
}
}
});
}, [tileUris, injectedItem, lastVisibleIndex, pageSize]);
const getInjectedItem = (index) => {
if (injectedItem && injectedItem.node) {
if (typeof injectedItem.node === 'function') {
@ -228,9 +238,14 @@ export default function ClaimList(props: Props) {
<>
<section ref={listRef} className={classnames('claim-grid', { 'swipe-list': swipeLayout })}>
{urisLength > 0 &&
tileUris.map((uri, index) => (
tileUris.map((uri, index) => {
if (uri) {
const inj = getInjectedItem(index);
return (
<React.Fragment key={uri}>
{getInjectedItem(index)}
{inj && inj}
{(index < tileUris.length - uriBuffer.length ||
(pageSize && index < pageSize - uriBuffer.length)) && (
<ClaimPreviewTile
uri={uri}
showHiddenByUser={showHiddenByUser}
@ -240,9 +255,13 @@ export default function ClaimList(props: Props) {
fypId={fypId}
showNoSourceClaims={showNoSourceClaims}
swipeLayout={swipeLayout}
onHidden={onHidden}
/>
)}
</React.Fragment>
))}
);
}
})}
{!timedOut && urisLength === 0 && !loading && !noEmpty && (
<div className="empty main--empty">{empty || noResultMsg}</div>
)}

View file

@ -732,7 +732,7 @@ function ClaimListDiscover(props: Props) {
{loading && useSkeletonScreen && (
<div className="claim-grid">
{new Array(dynamicPageSize).fill(1).map((x, i) => (
<ClaimPreviewTile key={i} placeholder="loading" />
<ClaimPreviewTile key={i} placeholder="loading" pulse />
))}
</div>
)}

View file

@ -55,6 +55,8 @@ type Props = {
isLivestreamActive: boolean,
livestreamViewerCount: ?number,
swipeLayout: boolean,
onHidden?: (string) => void,
pulse?: boolean,
};
// preview image cards used in related video functionality, channel overview page and homepage
@ -86,6 +88,8 @@ function ClaimPreviewTile(props: Props) {
mediaDuration,
viewCount,
swipeLayout = false,
onHidden,
pulse,
} = props;
const isRepost = claim && claim.repost_channel_url;
const isCollection = claim && claim.value_type === 'collection';
@ -158,6 +162,7 @@ function ClaimPreviewTile(props: Props) {
banState.filtered ||
(!showHiddenByUser && (banState.muted || banState.blocked)) ||
(isAbandoned && !showUnresolvedClaims));
if (onHidden && shouldHide) onHidden(props.uri);
}
if (shouldHide || (isLivestream && !showNoSourceClaims)) {
@ -173,6 +178,7 @@ function ClaimPreviewTile(props: Props) {
<li
className={classnames('placeholder claim-preview--tile', {
'swipe-list__item claim-preview--horizontal-tile': swipeLayout,
pulse: pulse,
})}
>
<div className="media__thumb">

View file

@ -1,6 +1,6 @@
// @flow
import type { Node } from 'react';
import React from 'react';
import React, { useState } from 'react';
import Button from 'component/button';
import ClaimPreviewTile from 'component/claimPreviewTile';
import I18nMessage from 'component/i18nMessage';
@ -52,6 +52,7 @@ type Props = {
hasSource?: boolean,
hasNoSource?: boolean,
forceShowReposts?: boolean, // overrides SETTINGS.HIDE_REPOSTS
loading: boolean,
// --- select ---
location: { search: string },
claimSearchResults: Array<string>,
@ -90,6 +91,7 @@ function ClaimTilesDiscover(props: Props) {
doFetchUserMemberships,
doResolveClaimIds,
doResolveUris,
loading,
} = props;
const listRef = React.useRef();
@ -100,6 +102,7 @@ function ClaimTilesDiscover(props: Props) {
const claimSearchUris = claimSearchResults || [];
const isUnfetchedClaimSearch = claimSearchResults === undefined;
const resolvedPinUris = useResolvePins({ pins, claimsById, doResolveClaimIds, doResolveUris });
const [uriBuffer, setUriBuffer] = useState([]);
const timedOut = claimSearchResults === null;
const shouldPerformSearch = !fetchingClaimSearch && !timedOut && claimSearchUris.length === 0;
@ -172,6 +175,19 @@ function ClaimTilesDiscover(props: Props) {
}
}, [doClaimSearch, shouldPerformSearch, optionsStringified]);
React.useEffect(() => {
finalUris.forEach((uri, index) => {
if (uri) {
const inj = getInjectedItem(index);
if (inj) {
if (uriBuffer.indexOf(index) === -1) {
setUriBuffer([index]);
}
}
}
});
}, [finalUris, injectedItem, lastVisibleIndex, pageSize]);
// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
@ -200,14 +216,14 @@ function ClaimTilesDiscover(props: Props) {
return (
<ul ref={listRef} className="claim-grid">
{finalUris && finalUris.length
{!loading && finalUris && finalUris.length
? finalUris.map((uri, i) => {
if (uri) {
const inj = getInjectedItem(i);
return (
<React.Fragment key={uri}>
{inj && inj}
{(!inj || !injectedItem || !injectedItem.replace) && (
{(i < finalUris.length - uriBuffer.length || i < pageSize - uriBuffer.length) && (
<ClaimPreviewTile
showNoSourceClaims={hasNoSource || showNoSourceClaims}
uri={uri}
@ -217,13 +233,15 @@ function ClaimTilesDiscover(props: Props) {
</React.Fragment>
);
} else {
return <ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder />;
return (
<ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder pulse />
);
}
})
: new Array(pageSize)
.fill(1)
.map((x, i) => (
<ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder />
<ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder pulse />
))}
</ul>
);
@ -236,6 +254,7 @@ export default React.memo<Props>(ClaimTilesDiscover, areEqual);
function trace(key, value) {
// @if process.env.DEBUG_TILE_RENDER
// $FlowFixMe "cannot coerce certain types".
console.log(`[claimTilesDiscover] ${key}: ${value}`); // eslint-disable-line no-console
// @endif

View file

@ -0,0 +1,6 @@
import { connect } from 'react-redux';
import PremiumPlusTile from './view';
const select = (state) => ({});
export default connect(select, {})(PremiumPlusTile);

View file

@ -0,0 +1,68 @@
// @flow
import React from 'react';
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import Icon from 'component/common/icon';
type Props = {
tileLayout?: boolean,
};
const PremiumPlusTile = (props: Props) => {
const { tileLayout } = props;
const title = __('No ads and access to exclusive features!');
const channel = __('Get Odysee Premium+');
const time = __('Now');
return tileLayout ? (
<li className="card claim-preview--tile claim-preview--premium-plus">
<a href={`/$/${PAGES.ODYSEE_MEMBERSHIP}`}>
<div className="media__thumb" />
<div className="claim-tile__header">
<h2 className="claim-tile__title">{title}</h2>
</div>
<div>
<div className="claim-tile__info">
<Icon icon={ICONS.UPGRADE} />
<div className="claim-tile__about">
<div className="channel-name">{channel}</div>
<div className="claim-tile__about--counts">
<span className="date_time">{time}</span>
</div>
</div>
</div>
</div>
</a>
</li>
) : (
<li className="claim-preview__wrapper claim-preview--premium-plus">
<a href={`/$/${PAGES.ODYSEE_MEMBERSHIP}`}>
<div className="claim-preview">
<div className="media__thumb" />
<div className="claim-preview__text">
<div className="claim-preview-metadata">
<div className="claim-preview-info">
<div className="claim-preview__title">{title}</div>
</div>
<div className="claim-tile__info">
<div className="claim-preview__channel-staked">
<Icon icon={ICONS.UPGRADE} />
</div>
<div className="media__subtitle">
<div className="button__content">
<span className="channel-name">{channel}</span>
<br />
</div>
<span sclassName="view_count">{time}</span>
</div>
</div>
</div>
</div>
</div>
</a>
</li>
);
};
export default PremiumPlusTile;

View file

@ -1,5 +1,5 @@
// @flow
import React from 'react';
import React, { useState } from 'react';
import Button from 'component/button';
import ClaimList from 'component/claimList';
import ClaimPreviewTile from 'component/claimPreviewTile';
@ -45,6 +45,22 @@ export default function RecommendedPersonal(props: Props) {
const count = personalRecommendations.uris.length;
const countCollapsed = getSuitablePageSizeForScreen(12, isLargeScreen, isMediumScreen);
const finalCount = view === VIEW.ALL_VISIBLE ? count : view === VIEW.COLLAPSED ? countCollapsed : 36;
const [hiddenArray, setHiddenArray] = useState([]);
function onClaimHidden(hiddenUri) {
let newArray = hiddenArray;
if (newArray.indexOf(hiddenUri) === -1) {
newArray.push(hiddenUri);
setHiddenArray(newArray);
}
}
function getHidden() {
let hidden = hiddenArray.length;
for (let uri of hiddenArray) {
if (personalRecommendations.uris.indexOf(uri) > finalCount) hidden--;
}
return hidden;
}
// **************************************************************************
// Effects
@ -147,8 +163,9 @@ export default function RecommendedPersonal(props: Props) {
<ClaimList
tileLayout
uris={personalRecommendations.uris.slice(0, finalCount)}
uris={personalRecommendations.uris.slice(0, finalCount + getHidden())}
fypId={personalRecommendations.gid}
onHidden={onClaimHidden}
/>
{view !== VIEW.ALL_VISIBLE && (

View file

@ -23,6 +23,7 @@ type Props = {
doShowSnackBar: (string) => void,
};
/* NEKO MARK */
const ScheduledStreams = (props: Props) => {
const {
channelIds,
@ -34,6 +35,7 @@ const ScheduledStreams = (props: Props) => {
onLoad,
showHideSetting = true,
} = props;
const isMobileScreen = useIsMobile();
const isLargeScreen = useIsLargeScreen();

View file

@ -8,6 +8,8 @@ import { selectActiveLivestreams } from 'redux/selectors/livestream';
import { selectFollowedTags } from 'redux/selectors/tags';
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
import { selectClientSetting, selectLanguage } from 'redux/selectors/settings';
import { selectAdBlockerFound } from 'redux/selectors/app';
import { selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
import DiscoverPage from './view';
const select = (state, props) => {
@ -23,6 +25,8 @@ const select = (state, props) => {
activeLivestreams: selectActiveLivestreams(state),
languageSetting: selectLanguage(state),
searchInLanguage: selectClientSetting(state, SETTINGS.SEARCH_IN_LANGUAGE),
adBlockerFound: selectAdBlockerFound(state),
hasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
};
};

View file

@ -19,6 +19,7 @@ import LbcSymbol from 'component/common/lbc-symbol';
import I18nMessage from 'component/i18nMessage';
import moment from 'moment';
import LivestreamSection from './livestreamSection';
import PremiumPlusTile from 'component/premiumPlusTile';
const CATEGORY_CONTENT_TYPES_FILTER = CS.CONTENT_TYPES.filter((x) => x !== CS.CLAIM_REPOST);
@ -35,6 +36,8 @@ type Props = {
tileLayout: boolean,
activeLivestreams: ?LivestreamInfo,
doFetchActiveLivestreams: (orderBy: ?Array<string>, lang: ?Array<string>) => void,
adBlockerFound: ?boolean,
hasPremiumPlus: ?boolean,
};
function DiscoverPage(props: Props) {
@ -51,6 +54,8 @@ function DiscoverPage(props: Props) {
activeLivestreams,
doFetchActiveLivestreams,
dynamicRouteProps,
adBlockerFound,
hasPremiumPlus,
} = props;
const buttonRef = useRef();
@ -221,7 +226,16 @@ function DiscoverPage(props: Props) {
tags={tags}
hiddenNsfwMessage={<HiddenNsfw type="page" />}
repostedClaimId={repostedClaim ? repostedClaim.claim_id : null}
injectedItem={!isWildWest && { node: <Ads small type="video" tileLayout={tileLayout} /> }}
injectedItem={
!isWildWest &&
!hasPremiumPlus && {
node: adBlockerFound ? (
<PremiumPlusTile tileLayout={tileLayout} />
) : (
<Ads small type="video" tileLayout />
),
}
}
// TODO: find a better way to determine discover / wild west vs other modes release times
// for now including && !tags so that
releaseTime={releaseTime || undefined}

View file

@ -5,7 +5,12 @@ import { doFetchActiveLivestreams } from 'redux/actions/livestream';
import { selectAdBlockerFound } from 'redux/selectors/app';
import { selectActiveLivestreams, selectFetchingActiveLivestreams } from 'redux/selectors/livestream';
import { selectFollowedTags } from 'redux/selectors/tags';
import { selectHasOdyseeMembership, selectHomepageFetched, selectUserVerifiedEmail } from 'redux/selectors/user';
import {
selectOdyseeMembershipIsPremiumPlus,
selectHasOdyseeMembership,
selectHomepageFetched,
selectUserVerifiedEmail,
} from 'redux/selectors/user';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import {
selectShowMatureContent,
@ -30,6 +35,7 @@ const select = (state) => ({
adBlockerFound: selectAdBlockerFound(state),
homepageOrder: selectClientSetting(state, SETTINGS.HOMEPAGE_ORDER),
hasMembership: selectHasOdyseeMembership(state),
hasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
});
const perform = (dispatch) => ({

View file

@ -20,6 +20,7 @@ import { splitBySeparator } from 'util/lbryURI';
import classnames from 'classnames';
import Ads from 'web/component/ads';
import Meme from 'web/component/meme';
import PremiumPlusTile from 'component/premiumPlusTile';
const FYP_SECTION: RowDataItem = {
id: 'FYP',
@ -46,6 +47,7 @@ type Props = {
homepageOrder: HomepageOrder,
doOpenModal: (id: string, ?{}) => void,
hasMembership: ?boolean,
hasPremiumPlus: ?boolean,
};
function HomePage(props: Props) {
@ -65,6 +67,7 @@ function HomePage(props: Props) {
homepageOrder,
doOpenModal,
hasMembership,
hasPremiumPlus,
} = props;
const showPersonalizedChannels = (authenticated || !IS_WEB) && subscribedChannels && subscribedChannels.length > 0;
@ -163,12 +166,14 @@ function HomePage(props: Props) {
prefixUris={getLivestreamUris(activeLivestreams, options.channelIds)}
pins={{ urls: pinUrls, claimIds: pinnedClaimIds }}
injectedItem={
index === 0 && {
node: <Ads small type="video" tileLayout />,
replace: adBlockerFound === false && isLargeScreen,
index === 0 &&
!hasPremiumPlus && {
node: adBlockerFound ? <PremiumPlusTile tileLayout /> : <Ads small type="video" tileLayout />,
}
}
forceShowReposts={id !== 'FOLLOWING'}
loading={id === 'FOLLOWING' ? fetchingActiveLivestreams : false}
adBlockerFound={adBlockerFound}
/>
);

View file

@ -33,10 +33,17 @@
aspect-ratio: 16 / 9;
$minWidth: calc(var(--file-list-thumbnail-width) * 0.8);
min-width: $minWidth;
background-color: #283263;
background-image: url('https://odysee.com/public/img/astronaut_n_friends.png');
background-size: 100%;
border-radius: var(--border-radius);
box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 0.1) inset;
video {
width: 100% !important;
height: 100% !important;
border-radius: var(--border-radius) !important;
border: 1px solid rgba(var(--color-primary-dynamic), 0.1);
}
@media (max-width: $breakpoint-small) {
@ -93,6 +100,33 @@
display: none !important;
}
}
&:hover {
.ad__container {
box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 1) inset;
}
video {
border: 1px solid rgba(var(--color-primary-dynamic), 1);
}
}
}
.claim-list {
.ads__claim-item--tile {
background-color: var(--color-ads-background);
margin-left: unset;
margin-bottom: var(--spacing-xxs);
width: 100%;
padding: var(--spacing-m);
flex: 1;
flex-direction: row;
.ad__container {
width: calc(var(--file-list-thumbnail-width) * 1.2);
// margin-left: var(--spacing-s);
margin-right: var(--spacing-s);
}
}
}
.ads__claim-item--tile {
@ -105,10 +139,30 @@
border-bottom: unset;
.ads__claim-text {
margin: var(--spacing-s) 0 0 0;
margin: var(--spacing-xs) 0 0 0;
display: flex;
flex-direction: column;
justify-content: center;
.ads__title {
color: var(--color-text);
}
.ads__subtitle {
margin-top: var(--spacing-s);
color: rgba(var(--color-text-base), 0.6);
font-weight: var(--font-weight-bold);
line-height: 1rem;
font-size: var(--font-xsmall);
.icon {
color: var(--color-text);
width: 2.1rem;
height: 2.1rem;
float: left;
margin-right: var(--spacing-s);
}
}
}
.ad__container {
@ -118,10 +172,52 @@
.ads__claim-text {
max-width: 100%;
}
#close-btn {
left: unset !important;
right: 0 !important;
border: none !important;
border-radius: 0 var(--border-radius) 0 var(--border-radius) !important;
background-color: var(--color-primary) !important;
}
.visible {
display: none !important;
position: absolute !important;
top: 0px !important;
z-index: 99 !important;
width: 3rem !important;
height: 3rem !important;
background-color: var(--color-primary) !important;
border-radius: 0 var(--border-radius) 0 var(--border-radius) !important;
background-size: 60% !important;
}
}
.ads__claim-item--recommended {
padding: var(--spacing-s);
// padding: var(--spacing-s);
padding: 0;
margin-top: var(--spacing-m);
border-radius: var(--border-radius);
border-bottom: unset;
.ad__container {
// width:100%;
width: var(--file-list-thumbnail-width);
}
.ads__claim-text {
margin: 0;
padding-left: var(--spacing-s);
.ads__title {
color: white;
}
}
.icon {
display: none;
}
@media (min-width: $breakpoint-medium) {
margin-bottom: 0;
@ -203,8 +299,66 @@
// Outbrain
// ****************************************************************************
.OUTBRAIN {
border-radius: var(--border-radius) 0 0 0 !important;
height: unset !important;
.ob-widget .ob-unit.ob-rec-text {
font-size: 12px !important;
}
}
.closeButton {
background-color: var(--color-primary) !important;
color: var(--color-primary-contrast) !important;
border-radius: var(--border-radius) var(--border-radius) 0 0 !important;
top: -27px !important;
width: 2rem !important;
padding-top: 3px;
// padding-bottom:4px !important;
height: 27px !important;
}
.ob-widget-footer {
position: absolute !important;
right: 32px;
top: -27px;
background: black;
border-radius: var(--border-radius) var(--border-radius) 0 0 !important;
padding-left: 4px;
padding-right: 6px;
padding-top: 3px;
padding-bottom: 0px;
.ob_what_resp {
padding: unset !important;
}
}
.ob-widget-items-container {
padding-left: var(--spacing-xs);
padding-right: var(--spacing-xs);
border-radius: var(--border-radius);
border-radius: var(--border-radius) 0 0 0;
}
// ****************************************************************************
// Neko Patch
// ****************************************************************************
.ad__container {
div {
max-width: unset !important;
}
}
#av-container #av-inner #gui::before {
background: unset !important;
#timeline #timeline-progress {
background: var(--color-primary) !important;
}
}
#av-container #av-inner #gui #timeline #timeline-progress {
background: var(--color-primary) !important;
}

View file

@ -397,7 +397,7 @@
flex-shrink: 0;
margin-right: var(--spacing-s);
box-shadow: 0px 0px 0px 1px rgba(var(--color-primary-dynamic), 0.1) inset;
box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 0.1) inset;
}
.media__thumb-placeholder-text {
@ -413,7 +413,7 @@
// show watch later button and duration divs when hovered
&:hover {
.media__thumb {
box-shadow: 0px 0px 0px 1px rgba(var(--color-primary-dynamic), 1) inset;
box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 1) inset;
}
.claim-preview__title {
color: var(--color-link);
@ -447,6 +447,29 @@
}
}
.claim-preview--premium-plus {
.media__thumb {
background-color: #283263;
background-image: url('https://odysee.com/public/img/astronaut_n_friends.png');
background-size: 100%;
}
.icon {
color: var(--color-text);
width: 2.1rem;
height: 2.1rem;
float: left;
margin-right: var(--spacing-s);
}
.channel-name {
font-weight: var(--font-weight-bold);
&:hover {
color: var(--color-text);
}
}
}
.claim-preview__empty {
display: flex;
align-items: center;
@ -938,11 +961,20 @@
}
}
.comment__badge {
display: inherit;
.icon {
margin-bottom: -1px;
padding-bottom: 4px;
}
}
&:hover {
cursor: pointer;
.media__thumb {
box-shadow: 0px 0px 0px 1px rgba(var(--color-primary-dynamic), 1) inset;
box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 1) inset;
background-size: 108%;
}
@ -965,7 +997,7 @@
color: var(--color-primary);
}
.media__thumb {
box-shadow: 0px 0px 0px 1px rgba(var(--color-primary-dynamic), 1) inset;
box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 1) inset;
}
}
}
@ -1332,7 +1364,7 @@ $claim-preview-progress-bar-height: 5px;
margin-bottom: -4px;
margin-left: 4px;
margin-right: var(--spacing-xxs);
text-overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
@ -1349,7 +1381,7 @@ $claim-preview-progress-bar-height: 5px;
}
.media__thumb {
box-shadow: 0px 0px 0px 1px rgba(var(--color-primary-dynamic), 1) inset;
box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 1) inset;
}
.claim-preview:not(.claim-preview--collection-mine):before {
@ -1371,14 +1403,13 @@ $claim-preview-progress-bar-height: 5px;
}
.claim-preview__live {
// margin-bottom: 0 !important;
.claim-preview__file-property-overlay {
opacity: 1; // The original 0.7 is not visible over bright thumbnails
opacity: 0.9; // The original 0.7 is not visible over bright thumbnails
color: var(--color-white-alt);
background-color: var(--color-live);
.claim-preview__overlay-properties {
margin-bottom: -2px;
color: white;
font-weight: var(--font-weight-bold);
}
}

View file

@ -84,7 +84,7 @@
align-items: center;
.icon {
margin-left: var(--spacing-xs);
margin-left: var(--spacing-xxs);
}
}

View file

@ -10,7 +10,7 @@
background-position: center;
background-repeat: no-repeat;
background-size: cover;
box-shadow: 0px 0px 0px 1px rgba(var(--color-primary-dynamic), 0.1) inset;
box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 0.1) inset;
}
// M E D I A

View file

@ -44,7 +44,7 @@
.section__header--actions {
padding-top: var(--spacing-m);
margin-top: var(--spacing-m);
margin-bottom: var(--spacing-m);
margin-bottom: var(--spacing-l);
display: flex;
align-items: flex-start;
justify-content: space-between;

View file

@ -188,6 +188,10 @@
border-width: 0;
}
.pulse {
animation: pulse 2s infinite ease-in-out;
}
@mixin mediaThumbHoverZoom {
.media__thumb,
img {

View file

@ -160,8 +160,8 @@
--color-scrollbar-track-bg: transparent;
--color-body-scrollbar-track-bg: rgba(0, 0, 0, 0.5);
--background-shade: linear-gradient(-180deg, rgba(25, 25, 25, 0.2) 2%, #202020),
radial-gradient(circle at 50% 117%, rgba(25, 25, 25, 0.2) 0, #202020 100%);
--background-shade: linear-gradient(-180deg, rgba(25, 25, 25, 0.3) 2%, #202020),
radial-gradient(circle at 50% 117%, rgba(25, 25, 25, 0.4) 0, #202020 100%);
--mui-background: #000;
}

View file

@ -6,6 +6,8 @@ import I18nMessage from 'component/i18nMessage';
import Button from 'component/button';
import classnames from 'classnames';
import { platform } from 'util/platform';
import Icon from 'component/common/icon';
import * as ICONS from 'constants/icons';
// prettier-ignore
const AD_CONFIGS = Object.freeze({
@ -124,7 +126,7 @@ function Ads(props: Props) {
),
}}
>
Hate these? %sign_up_for_premium% for an ad free experience.
%sign_up_for_premium% for an ad free experience.
</I18nMessage>
);
@ -143,8 +145,16 @@ function Ads(props: Props) {
'ads__claim-text--small': small,
})}
>
<div>Ad</div>
<p>{adsSignInDriver}</p>
<div className="ads__title">
{__('Ad')}
<br />
{__('Hate these?')}
{/* __('No ads, a custom badge and access to exclusive features, try Odysee Premium!') */}
</div>
<div className="ads__subtitle">
<Icon icon={ICONS.UPGRADE} />
{adsSignInDriver}
</div>
</div>
</div>
);