diff --git a/.flowconfig b/.flowconfig index 5f5dfd123..899a096e6 100644 --- a/.flowconfig +++ b/.flowconfig @@ -18,6 +18,5 @@ module.name_mapper='^types\(.*\)$' -> '/src/renderer/types\1' module.name_mapper='^component\(.*\)$' -> '/src/renderer/component\1' module.name_mapper='^page\(.*\)$' -> '/src/renderer/page\1' module.name_mapper='^lbry\(.*\)$' -> '/src/renderer/lbry\1' -module.name_mapper='^modal\(.*\)$' -> '/src/renderer/modal\1' [strict] diff --git a/flow-typed/react-modal.js b/flow-typed/react-modal.js deleted file mode 100644 index 17766e9f0..000000000 --- a/flow-typed/react-modal.js +++ /dev/null @@ -1,3 +0,0 @@ -declare module 'react-modal' { - declare module.exports: any; -} diff --git a/package.json b/package.json index 07d4e84fd..2e61b99cc 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "amplitude-js": "^4.0.0", "bluebird": "^3.5.1", "classnames": "^2.2.5", - "dom-scroll-into-view": "^1.2.1", "electron-dl": "^1.6.0", "formik": "^0.10.4", "from2": "^2.3.0", diff --git a/src/renderer/component/app/view.jsx b/src/renderer/component/app/view.jsx index 240db13e2..fb43b6574 100644 --- a/src/renderer/component/app/view.jsx +++ b/src/renderer/component/app/view.jsx @@ -1,4 +1,3 @@ -// @flow import React from 'react'; import Router from 'component/router/index'; import Header from 'component/header'; @@ -7,83 +6,61 @@ import ModalRouter from 'modal/modalRouter'; import ReactModal from 'react-modal'; import throttle from 'util/throttle'; -type Props = { - alertError: (string | {}) => void, - recordScroll: number => void, - currentStackIndex: number, - currentPageAttributes: { path: string, scrollY: number }, - pageTitle: ?string, -}; - -class App extends React.PureComponent { +class App extends React.PureComponent { constructor() { super(); this.mainContent = undefined; - (this: any).scrollListener = this.scrollListener.bind(this); } componentWillMount() { const { alertError } = this.props; - // TODO: create type for this object - // it lives in jsonrpc.js - document.addEventListener('unhandledError', (event: any) => { + document.addEventListener('unhandledError', event => { alertError(event.detail); }); } componentDidMount() { + const { recordScroll } = this.props; const mainContent = document.getElementById('main-content'); this.mainContent = mainContent; - if (this.mainContent) { - this.mainContent.addEventListener('scroll', throttle(this.scrollListener, 750)); - } + const scrollListener = () => recordScroll(this.mainContent.scrollTop); + + this.mainContent.addEventListener('scroll', throttle(scrollListener, 750)); ReactModal.setAppElement('#window'); // fuck this } - componentWillReceiveProps(props: Props) { - const { pageTitle } = props; - this.setTitleFromProps(pageTitle); + componentWillUnmount() { + this.mainContent.removeEventListener('scroll', this.scrollListener); } - componentDidUpdate(prevProps: Props) { + componentWillReceiveProps(props) { + this.setTitleFromProps(props); + } + + componentDidUpdate(prevProps) { const { currentStackIndex: prevStackIndex } = prevProps; const { currentStackIndex, currentPageAttributes } = this.props; - if (this.mainContent && currentStackIndex !== prevStackIndex) { + if (currentStackIndex !== prevStackIndex) { this.mainContent.scrollTop = currentPageAttributes.scrollY || 0; } } - componentWillUnmount() { - if (this.mainContent) { - // having issues with this - // $FlowFixMe - this.mainContent.removeEventListener('scroll'); - } + setTitleFromProps(props) { + window.document.title = props.pageTitle || 'LBRY'; } - setTitleFromProps = (title: ?string) => { - window.document.title = title || 'LBRY'; - }; - - scrollListener() { - const { recordScroll } = this.props; - if (this.mainContent) { - recordScroll(this.mainContent.scrollTop); - } - } - - mainContent: ?HTMLElement; - render() { return (
- +
+ +
); diff --git a/src/renderer/component/cardMedia/view.jsx b/src/renderer/component/cardMedia/view.jsx index 34feddccb..06f80f1d7 100644 --- a/src/renderer/component/cardMedia/view.jsx +++ b/src/renderer/component/cardMedia/view.jsx @@ -1,18 +1,48 @@ -// @flow import React from 'react'; -type Props = { - thumbnail: ?string, // externally sourced image -}; +class CardMedia extends React.PureComponent { + static AUTO_THUMB_CLASSES = [ + 'purple', + 'red', + 'pink', + 'indigo', + 'blue', + 'light-blue', + 'cyan', + 'teal', + 'green', + 'yellow', + 'orange', + ]; + + componentWillMount() { + this.setState({ + autoThumbClass: + CardMedia.AUTO_THUMB_CLASSES[ + Math.floor(Math.random() * CardMedia.AUTO_THUMB_CLASSES.length) + ], + }); + } -class CardMedia extends React.PureComponent { render() { - const { thumbnail } = this.props; + const { title, thumbnail } = this.props; + const atClass = this.state.autoThumbClass; + if (thumbnail) { return
; } - return
LBRY
; + return ( +
+
+ {title && + title + .replace(/\s+/g, '') + .substring(0, Math.min(title.replace(' ', '').length, 5)) + .toUpperCase()} +
+
+ ); } } diff --git a/src/renderer/component/common.js b/src/renderer/component/common.js index 687c6e9da..629d09f57 100644 --- a/src/renderer/component/common.js +++ b/src/renderer/component/common.js @@ -1,6 +1,3 @@ -// just disabling the linter because this file shouldn't even exist -// will continue to move components over to /components/common/{comp} - sean -/* eslint-disable */ import React from 'react'; import PropTypes from 'prop-types'; import { formatCredits, formatFullPrice } from 'util/formatCredits'; @@ -173,4 +170,3 @@ export class Thumbnail extends React.PureComponent { ); } } -/* eslint-enable */ diff --git a/src/renderer/component/common/category-list.jsx b/src/renderer/component/common/category-list.jsx deleted file mode 100644 index 1f01ea921..000000000 --- a/src/renderer/component/common/category-list.jsx +++ /dev/null @@ -1,255 +0,0 @@ -// @flow -import React from 'react'; -import lbryuri from 'lbryuri'; -import ToolTip from 'component/common/tooltip'; -import FileCard from 'component/fileCard'; -import Button from 'component/link'; - -type Props = { - category: string, - names: Array, - categoryLink?: string, -}; - -type State = { - canScrollNext: boolean, - canScrollPrevious: boolean, -}; - -class CategoryList extends React.PureComponent { - constructor() { - super(); - - this.state = { - canScrollPrevious: false, - canScrollNext: false, - }; - - (this: any).handleScrollNext = this.handleScrollNext.bind(this); - (this: any).handleScrollPrevious = this.handleScrollPrevious.bind(this); - this.rowItems = undefined; - } - - componentDidMount() { - const cardRow = this.rowItems; - if (cardRow) { - const cards = cardRow.getElementsByTagName('section'); - const lastCard = cards[cards.length - 1]; - const isCompletelyVisible = this.isCardVisible(lastCard); - - if (!isCompletelyVisible) { - // not sure how we can avoid doing this - /* eslint-disable react/no-did-mount-set-state */ - this.setState({ - canScrollNext: true, - }); - /* eslint-enable react/no-did-mount-set-state */ - } - } - } - - rowItems: ?HTMLDivElement; - - handleScroll(cardRow: HTMLDivElement, scrollTarget: number) { - const cards = cardRow.getElementsByTagName('section'); - const animationCallback = () => { - const firstCard = cards[0]; - const lastCard = cards[cards.length - 1]; - const firstCardVisible = this.isCardVisible(firstCard); - const lastCardVisible = this.isCardVisible(lastCard); - this.setState({ - canScrollNext: !lastCardVisible, - canScrollPrevious: !firstCardVisible, - }); - }; - - const currentScrollLeft = cardRow.scrollLeft; - const direction = currentScrollLeft > scrollTarget ? 'left' : 'right'; - this.scrollCardsAnimated(cardRow, scrollTarget, direction, animationCallback); - } - - scrollCardsAnimated = ( - cardRow: HTMLDivElement, - scrollTarget: number, - direction: string, - callback: () => any - ) => { - let start; - const step = timestamp => { - if (!start) start = timestamp; - - const currentLeftVal = cardRow.scrollLeft; - - let newTarget; - let shouldContinue; - let progress = currentLeftVal; - - if (direction === 'right') { - progress += timestamp - start; - newTarget = Math.min(progress, scrollTarget); - shouldContinue = newTarget < scrollTarget; - } else { - progress -= timestamp - start; - newTarget = Math.max(progress, scrollTarget); - shouldContinue = newTarget > scrollTarget; - } - - cardRow.scrollLeft = newTarget; // eslint-disable-line no-param-reassign - - if (shouldContinue) { - window.requestAnimationFrame(step); - } else { - callback(); - } - }; - - window.requestAnimationFrame(step); - }; - - // check if a card is fully visible horizontally - isCardVisible = (section: HTMLElement) => { - const rect = section.getBoundingClientRect(); - const isVisible = rect.left >= 0 && rect.right <= window.innerWidth; - return isVisible; - }; - - handleScrollNext() { - const cardRow = this.rowItems; - if (cardRow) { - const cards = cardRow.getElementsByTagName('section'); - - // loop over items until we find one that is on the screen - // continue searching until a card isn't fully visible, this is the new target - let firstFullVisibleCard; - let firstSemiVisibleCard; - - for (let i = 0; i < cards.length; i += 1) { - const currentCardVisible = this.isCardVisible(cards[i]); - - if (firstFullVisibleCard && !currentCardVisible) { - firstSemiVisibleCard = cards[i]; - break; - } else if (currentCardVisible) { - [firstFullVisibleCard] = cards; - } - } - - if (firstFullVisibleCard && firstSemiVisibleCard) { - const scrollTarget = firstSemiVisibleCard.offsetLeft - firstFullVisibleCard.offsetLeft; - this.handleScroll(cardRow, scrollTarget); - } - } - } - - handleScrollPrevious() { - const cardRow = this.rowItems; - if (cardRow) { - const cards = cardRow.getElementsByTagName('section'); - - let hasFoundCard; - let numberOfCardsThatCanFit = 0; - - // loop starting at the end until we find a visible card - // then count to find how many cards can fit on the screen - for (let i = cards.length - 1; i >= 0; i -= 1) { - const currentCard = cards[i]; - const isCurrentCardVisible = this.isCardVisible(currentCard); - - if (isCurrentCardVisible) { - if (!hasFoundCard) { - hasFoundCard = true; - } - - numberOfCardsThatCanFit += 1; - } else if (hasFoundCard) { - // this card is off the screen to the left - // we know how many cards can fit on a screen - // find the new target and scroll - const firstCardOffsetLeft = cards[0].offsetLeft; - const cardIndexToScrollTo = i + 1 - numberOfCardsThatCanFit; - const newFirstCard = cards[cardIndexToScrollTo]; - - let scrollTarget; - if (newFirstCard) { - scrollTarget = newFirstCard.offsetLeft; - } else { - // more cards can fit on the screen than are currently hidden - // just scroll to the first card - scrollTarget = cards[0].offsetLeft; - } - - scrollTarget -= firstCardOffsetLeft; // to play nice with the margins - - this.handleScroll(cardRow, scrollTarget); - break; - } - } - } - } - - render() { - const { category, names, categoryLink } = this.props; - const { canScrollNext, canScrollPrevious } = this.state; - - // The lint was throwing an error saying we should use
-
-
- -
{ - this.rowItems = ref; - }} - className="card-row__scrollhouse" - > - {names && - names.map(name => ( - - ))} -
- - ); - } -} - -export default CategoryList; diff --git a/src/renderer/component/common/icon.jsx b/src/renderer/component/common/icon.jsx deleted file mode 100644 index 3e261d21b..000000000 --- a/src/renderer/component/common/icon.jsx +++ /dev/null @@ -1,43 +0,0 @@ -// @flow -import React from 'react'; -import classnames from 'classnames'; -import * as icons from 'constants/icons'; - -type Props = { - icon: string, - fixed?: boolean, - padded?: boolean, -}; - -class Icon extends React.PureComponent { - getIconTitle() { - const { icon } = this.props; - - switch (icon) { - case icons.FEATURED: - return __('Watch this and earn rewards.'); - case icons.LOCAL: - return __('You have a copy of this file.'); - default: - return ''; - } - } - - render() { - const { icon, fixed, padded } = this.props; - const iconClassName = icon.startsWith('icon-') ? icon : `icon-${icon}`; - const title = this.getIconTitle(); - - const spanClassName = classnames( - { - 'icon--fixed-width': fixed, - 'icon--padded': padded, - }, - iconClassName - ); - - return ; - } -} - -export default Icon; diff --git a/src/renderer/component/common/tooltip.jsx b/src/renderer/component/common/tooltip.jsx deleted file mode 100644 index 942ac73be..000000000 --- a/src/renderer/component/common/tooltip.jsx +++ /dev/null @@ -1,57 +0,0 @@ -// @flow -import React from 'react'; -import classnames from 'classnames'; -import Icon from 'component/common/icon'; -import Button from 'component/link'; - -type Props = { - body: string, - label: string, -}; - -type State = { - showTooltip: boolean, -}; - -class ToolTip extends React.PureComponent { - constructor(props: Props) { - super(props); - - this.state = { - showTooltip: false, - }; - - (this: any).handleClick = this.handleClick.bind(this); - } - - handleClick() { - const { showTooltip } = this.state; - - if (!showTooltip) { - document.addEventListener('click', this.handleClick); - } else { - document.removeEventListener('click', this.handleClick); - } - - this.setState({ - showTooltip: !showTooltip, - }); - } - - render() { - const { label, body } = this.props; - const { showTooltip } = this.state; - - return ( - - -
{body}
-
- ); - } -} - -export default ToolTip; diff --git a/src/renderer/component/fileCard/view.jsx b/src/renderer/component/fileCard/view.jsx index a3c8cc249..e38bf06c2 100644 --- a/src/renderer/component/fileCard/view.jsx +++ b/src/renderer/component/fileCard/view.jsx @@ -1,101 +1,111 @@ -// @flow import React from 'react'; -import lbryuri from 'lbryuri'; +import lbryuri from 'lbryuri.js'; import CardMedia from 'component/cardMedia'; +import Link from 'component/link'; import { TruncatedText } from 'component/common'; -import Icon from 'component/common/icon'; +import Icon from 'component/icon'; import FilePrice from 'component/filePrice'; import UriIndicator from 'component/uriIndicator'; import NsfwOverlay from 'component/nsfwOverlay'; +import TruncatedMarkdown from 'component/truncatedMarkdown'; import * as icons from 'constants/icons'; -import classnames from 'classnames'; -// TODO: iron these out -type Props = { - isResolvingUri: boolean, - resolveUri: string => void, - uri: string, - claim: ?{ claim_id: string }, - fileInfo: ?{}, - metadata: ?{ nsfw: boolean, thumbnail: ?string }, - navigate: (string, ?{}) => void, - rewardedContentClaimIds: Array, - obscureNsfw: boolean, -}; +class FileCard extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + hovered: false, + }; + } -class FileCard extends React.PureComponent { componentWillMount() { this.resolve(this.props); } - componentWillReceiveProps(nextProps: Props) { + componentWillReceiveProps(nextProps) { this.resolve(nextProps); } - resolve = (props: Props) => { + resolve(props) { const { isResolvingUri, resolveUri, claim, uri } = props; if (!isResolvingUri && claim === undefined && uri) { resolveUri(uri); } - }; + } + + handleMouseOver() { + this.setState({ + hovered: true, + }); + } + + handleMouseOut() { + this.setState({ + hovered: false, + }); + } render() { const { claim, fileInfo, metadata, + isResolvingUri, navigate, rewardedContentClaimIds, - obscureNsfw, } = this.props; + const uri = lbryuri.normalize(this.props.uri); const title = metadata && metadata.title ? metadata.title : uri; const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null; - const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw; + const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id); - // Come back to this on other pages - // let description = ''; - // if (isResolvingUri && !claim) { - // description = __('Loading...'); - // } else if (metadata && metadata.description) { - // description = metadata.description; - // } else if (claim === null) { - // description = __('This address contains no content.'); - // } + let description = ''; + if (isResolvingUri && !claim) { + description = __('Loading...'); + } else if (metadata && metadata.description) { + description = metadata.description; + } else if (claim === null) { + description = __('This address contains no content.'); + } - // We don't want to allow a click handler unless it's in focus - // I'll come back to this when I work on site-wide keyboard navigation - /* eslint-disable jsx-a11y/click-events-have-key-events */ return (
navigate('/show', { uri })} - className={classnames('card card--small card__link', { - 'card--obscured': shouldObscureNsfw, - })} + className={`card card--small card--link ${obscureNsfw ? 'card--obscured ' : ''}`} + onMouseEnter={this.handleMouseOver.bind(this)} + onMouseLeave={this.handleMouseOut.bind(this)} > - - -
-
- {title} -
- -
- -
- {isRewardContent && } - {fileInfo && } +
+ navigate('/show', { uri })} className="card__link"> + +
+
+ {title} +
+
+ + {' '} + {isRewardContent && }{' '} + {fileInfo && } + + + + +
-
+ + {/* Test for nizuka's design: should we remove description? +
+ {description} +
+ */}
- {obscureNsfw && } + {obscureNsfw && this.state.hovered && }
); - /* eslint-enable jsx-a11y/click-events-have-key-events */ } } diff --git a/src/renderer/component/fileDownloadLink/view.jsx b/src/renderer/component/fileDownloadLink/view.jsx index 71cf39018..57818da51 100644 --- a/src/renderer/component/fileDownloadLink/view.jsx +++ b/src/renderer/component/fileDownloadLink/view.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { BusyMessage } from 'component/common'; -import Icon from 'component/common/icon'; +import Icon from 'component/icon'; import Link from 'component/link'; class FileDownloadLink extends React.PureComponent { diff --git a/src/renderer/component/fileTile/view.jsx b/src/renderer/component/fileTile/view.jsx index ae1901485..231cfc293 100644 --- a/src/renderer/component/fileTile/view.jsx +++ b/src/renderer/component/fileTile/view.jsx @@ -1,4 +1,3 @@ -/* eslint-disable */ import React from 'react'; import * as icons from 'constants/icons'; import lbryuri from 'lbryuri.js'; @@ -6,7 +5,7 @@ import CardMedia from 'component/cardMedia'; import { TruncatedText } from 'component/common.js'; import FilePrice from 'component/filePrice'; import NsfwOverlay from 'component/nsfwOverlay'; -import Icon from 'component/common/icon'; +import Icon from 'component/icon'; class FileTile extends React.PureComponent { static SHOW_EMPTY_PUBLISH = 'publish'; @@ -134,4 +133,3 @@ class FileTile extends React.PureComponent { } export default FileTile; -/* eslint-enable */ diff --git a/src/renderer/component/form.js b/src/renderer/component/form.js index 889bec15a..c13619b1a 100644 --- a/src/renderer/component/form.js +++ b/src/renderer/component/form.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import FormField from 'component/formField'; -import Icon from 'component/common/icon'; +import Icon from 'component/icon'; let formFieldCounter = 0; diff --git a/src/renderer/component/header/view.jsx b/src/renderer/component/header/view.jsx index f8fc83890..c1289c889 100644 --- a/src/renderer/component/header/view.jsx +++ b/src/renderer/component/header/view.jsx @@ -1,20 +1,8 @@ -// @flow import React from 'react'; -import Button from 'component/link'; +import Link from 'component/link'; import WunderBar from 'component/wunderbar'; -type Props = { - balance: string, - back: any => void, - forward: any => void, - isBackDisabled: boolean, - isForwardDisabled: boolean, - isUpgradeAvailable: boolean, - navigate: any => void, - downloadUpgrade: any => void, -}; - -export const Header = (props: Props) => { +export const Header = props => { const { balance, back, @@ -27,58 +15,85 @@ export const Header = (props: Props) => { } = props; return ( ); }; diff --git a/src/renderer/component/icon/index.js b/src/renderer/component/icon/index.js new file mode 100644 index 000000000..81d61e58b --- /dev/null +++ b/src/renderer/component/icon/index.js @@ -0,0 +1,5 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import Icon from './view'; + +export default connect(null, null)(Icon); diff --git a/src/renderer/component/icon/view.jsx b/src/renderer/component/icon/view.jsx new file mode 100644 index 000000000..795a1b241 --- /dev/null +++ b/src/renderer/component/icon/view.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import * as icons from 'constants/icons'; +import classnames from 'classnames'; + +export default class Icon extends React.PureComponent { + static propTypes = { + icon: PropTypes.string.isRequired, + fixed: PropTypes.bool, + }; + + static defaultProps = { + fixed: false, + }; + + getIconClass() { + const { icon } = this.props; + + return icon.startsWith('icon-') ? icon : `icon-${icon}`; + } + + getIconTitle() { + switch (this.props.icon) { + case icons.FEATURED: + return __('Watch this and earn rewards.'); + case icons.LOCAL: + return __('You have a copy of this file.'); + default: + return ''; + } + } + + render() { + const { icon, fixed, className, leftPad } = this.props; + const iconClass = this.getIconClass(); + const title = this.getIconTitle(); + + const spanClassName = classnames( + 'icon', + iconClass, + { + 'icon-fixed-width': fixed, + 'icon--left-pad': leftPad, + }, + className + ); + + return ; + } +} diff --git a/src/renderer/component/inviteList/view.jsx b/src/renderer/component/inviteList/view.jsx index 79b344e4e..c339cfd1b 100644 --- a/src/renderer/component/inviteList/view.jsx +++ b/src/renderer/component/inviteList/view.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import Icon from 'component/common/icon'; +import Icon from 'component/icon'; import RewardLink from 'component/rewardLink'; import rewards from 'rewards.js'; diff --git a/src/renderer/component/link/view.jsx b/src/renderer/component/link/view.jsx index 41b103411..92451daff 100644 --- a/src/renderer/component/link/view.jsx +++ b/src/renderer/component/link/view.jsx @@ -1,99 +1,60 @@ -// @flow -import * as React from 'react'; -import Icon from 'component/common/icon'; -import classnames from 'classnames'; +import React from 'react'; +import Icon from 'component/icon'; -type Props = { - onClick: ?(any) => any, - href: ?string, - title: ?string, - label: ?string, - icon: ?string, - iconRight: ?string, - disabled: ?boolean, - children: ?React.Node, - navigate: ?string, - // TODO: these (nav) should be a reusable type - doNavigate: (string, ?any) => void, - navigateParams: any, - className: ?string, - inverse: ?boolean, - circle: ?boolean, - alt: ?boolean, - flat: ?boolean, - fakeLink: ?boolean, - description: ?string, -}; - -const Button = (props: Props) => { +const Link = props => { const { - onClick, href, title, + style, label, icon, iconRight, + button, disabled, children, navigate, navigateParams, doNavigate, className, - inverse, - alt, - circle, - flat, - fakeLink, - description, - ...otherProps + span, } = props; - const combinedClassName = classnames( - { - btn: !fakeLink, - 'btn--link': fakeLink, - 'btn--primary': !fakeLink && !alt, - 'btn--alt': alt, - 'btn--inverse': inverse, - 'btn--disabled': disabled, - 'btn--circle': circle, - 'btn--flat': flat, - }, - className - ); + const combinedClassName = + (className || '') + + (!className && !button ? 'button-text' : '') + // Non-button links get the same look as text buttons + (button ? ` button-block button-${button} button-set-item` : '') + + (disabled ? ' disabled' : ''); - const extendedOnClick = - !onClick && navigate + const onClick = + !props.onClick && navigate ? event => { event.stopPropagation(); doNavigate(navigate, navigateParams || {}); } - : onClick; + : props.onClick; - const content = ( - - {icon && } - {label && {label}} - {children && children} - {iconRight && } - - ); + let content; + if (children) { + content = children; + } else { + content = ( + + {icon ? : null} + {label ? {label} : null} + {iconRight ? : null} + + ); + } - return href ? ( -
- {content} - - ) : ( - - ); + const linkProps = { + className: combinedClassName, + href: href || 'javascript:;', + title, + onClick, + style, + }; + + return span ? {content} : {content}; }; -export default Button; +export default Link; diff --git a/src/renderer/component/menu.js b/src/renderer/component/menu.js index 5be125cd7..e070f8711 100644 --- a/src/renderer/component/menu.js +++ b/src/renderer/component/menu.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Icon from 'component/common/icon'; +import Icon from 'component/icon'; import Link from 'component/link'; export class DropDownMenuItem extends React.PureComponent { diff --git a/src/renderer/component/page/index.js b/src/renderer/component/page/index.js deleted file mode 100644 index dbe9cc315..000000000 --- a/src/renderer/component/page/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import { connect } from 'react-redux'; -import { selectPageTitle } from 'redux/selectors/navigation'; -import Page from './view'; - -const select = state => ({ - title: selectPageTitle(state), -}); - -export default connect(select, null)(Page); diff --git a/src/renderer/component/page/view.jsx b/src/renderer/component/page/view.jsx deleted file mode 100644 index 9d16b3e8f..000000000 --- a/src/renderer/component/page/view.jsx +++ /dev/null @@ -1,26 +0,0 @@ -// @flow -import * as React from 'react'; -import classnames from 'classnames'; -import { BusyMessage } from 'component/common'; - -type Props = { - children: React.Node, - title: ?string, - noPadding: ?boolean, - isLoading: ?boolean, -}; - -const Page = (props: Props) => { - const { children, title, noPadding, isLoading } = props; - return ( -
-
- {title &&

{title}

} - {isLoading && } -
-
{children}
-
- ); -}; - -export default Page; diff --git a/src/renderer/component/tooltip.js b/src/renderer/component/tooltip.js new file mode 100644 index 000000000..c81775614 --- /dev/null +++ b/src/renderer/component/tooltip.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export class ToolTip extends React.PureComponent { + static propTypes = { + body: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + showTooltip: false, + }; + } + + handleClick() { + this.setState({ + showTooltip: !this.state.showTooltip, + }); + } + + handleTooltipMouseOut() { + this.setState({ + showTooltip: false, + }); + } + + render() { + return ( + + { + this.handleClick(); + }} + > + {this.props.label} + +
{ + this.handleTooltipMouseOut(); + }} + > + {this.props.body} +
+
+ ); + } +} + +export default ToolTip; diff --git a/src/renderer/component/uriIndicator/view.jsx b/src/renderer/component/uriIndicator/view.jsx index 04a8c78a3..704769250 100644 --- a/src/renderer/component/uriIndicator/view.jsx +++ b/src/renderer/component/uriIndicator/view.jsx @@ -1,46 +1,35 @@ -// @flow import React from 'react'; -import { Icon } from 'component/common'; -import Button from 'component/link'; +import Icon from 'component/icon'; +import Link from 'component/link'; import lbryuri from 'lbryuri'; import classnames from 'classnames'; -type Props = { - isResolvingUri: boolean, - resolveUri: string => void, - claim: { - channel_name: string, - has_signature: boolean, - signature_is_valid: boolean, - value: { - publisherSignature: { certificateId: string }, - }, - }, - uri: string, - link: ?boolean, -}; - -class UriIndicator extends React.PureComponent { +class UriIndicator extends React.PureComponent { componentWillMount() { this.resolve(this.props); } - componentWillReceiveProps(nextProps: Props) { + componentWillReceiveProps(nextProps) { this.resolve(nextProps); } - resolve = (props: Props) => { + resolve(props) { const { isResolvingUri, resolveUri, claim, uri } = props; if (!isResolvingUri && claim === undefined && uri) { resolveUri(uri); } - }; + } render() { - const { claim, link, isResolvingUri } = this.props; + const { claim, link, uri, isResolvingUri, smallCard, span } = this.props; + + if (isResolvingUri && !claim) { + return Validating...; + } + if (!claim) { - return {isResolvingUri ? 'Validating...' : 'Unused'}; + return Unused; } const { @@ -49,17 +38,14 @@ class UriIndicator extends React.PureComponent { signature_is_valid: signatureIsValid, value, } = claim; - const channelClaimId = value && value.publisherSignature && value.publisherSignature.certificateId; if (!hasSignature || !channelName) { - return Anonymous; + return Anonymous; } - let icon; - let channelLink; - let modifier; + let icon, channelLink, modifier; if (signatureIsValid) { modifier = 'valid'; @@ -73,6 +59,7 @@ class UriIndicator extends React.PureComponent { @@ -94,9 +81,14 @@ class UriIndicator extends React.PureComponent { } return ( - + ); } } diff --git a/src/renderer/component/wunderbar/index.js b/src/renderer/component/wunderbar/index.js index 575676232..826ab4225 100644 --- a/src/renderer/component/wunderbar/index.js +++ b/src/renderer/component/wunderbar/index.js @@ -1,21 +1,19 @@ +import React from 'react'; import { connect } from 'react-redux'; -import lbryuri from 'lbryuri'; -import { selectState as selectSearch, selectWunderBarAddress } from 'redux/selectors/search'; +import lbryuri from 'lbryuri.js'; +import { selectWunderBarAddress, selectWunderBarIcon } from 'redux/selectors/search'; import { doNavigate } from 'redux/actions/navigation'; -import { updateSearchQuery, getSearchSuggestions } from 'redux/actions/search'; import Wunderbar from './view'; const select = state => ({ - ...selectSearch(state), address: selectWunderBarAddress(state), + icon: selectWunderBarIcon(state), }); const perform = dispatch => ({ onSearch: query => dispatch(doNavigate('/search', { query })), onSubmit: (query, extraParams) => dispatch(doNavigate('/show', { uri: lbryuri.normalize(query), ...extraParams })), - updateSearchQuery: query => dispatch(updateSearchQuery(query)), - getSearchSuggestions: query => dispatch(getSearchSuggestions(query)), }); export default connect(select, perform)(Wunderbar); diff --git a/src/renderer/component/wunderbar/internal/autocomplete.jsx b/src/renderer/component/wunderbar/internal/autocomplete.jsx deleted file mode 100644 index 5c70c8f06..000000000 --- a/src/renderer/component/wunderbar/internal/autocomplete.jsx +++ /dev/null @@ -1,601 +0,0 @@ -/* -This is taken from https://github.com/reactjs/react-autocomplete - -We aren't using that component because (for now) there is no way to autohightlight -the first item if it isn't an exact match from what is in the search bar. - -Our use case is: -value in search bar: "hello" -first suggestion: "lbry://hello" - -I changed the function maybeAutoCompleteText to check if the suggestion contains -the search query anywhere, instead of the suggestion starting with it - -https://github.com/reactjs/react-autocomplete/issues/239 -*/ -/* eslint-disable */ - -const React = require('react'); -const PropTypes = require('prop-types'); -const { findDOMNode } = require('react-dom'); -const scrollIntoView = require('dom-scroll-into-view'); - -const IMPERATIVE_API = [ - 'blur', - 'checkValidity', - 'click', - 'focus', - 'select', - 'setCustomValidity', - 'setSelectionRange', - 'setRangeText', -]; - -function getScrollOffset() { - return { - x: - window.pageXOffset !== undefined - ? window.pageXOffset - : (document.documentElement || document.body.parentNode || document.body).scrollLeft, - y: - window.pageYOffset !== undefined - ? window.pageYOffset - : (document.documentElement || document.body.parentNode || document.body).scrollTop, - }; -} - -export default class Autocomplete extends React.Component { - static propTypes = { - /** - * The items to display in the dropdown menu - */ - items: PropTypes.array.isRequired, - /** - * The value to display in the input field - */ - value: PropTypes.any, - /** - * Arguments: `event: Event, value: String` - * - * Invoked every time the user changes the input's value. - */ - onChange: PropTypes.func, - /** - * Arguments: `value: String, item: Any` - * - * Invoked when the user selects an item from the dropdown menu. - */ - onSelect: PropTypes.func, - /** - * Arguments: `item: Any, value: String` - * - * Invoked for each entry in `items` and its return value is used to - * determine whether or not it should be displayed in the dropdown menu. - * By default all items are always rendered. - */ - shouldItemRender: PropTypes.func, - /** - * Arguments: `itemA: Any, itemB: Any, value: String` - * - * The function which is used to sort `items` before display. - */ - sortItems: PropTypes.func, - /** - * Arguments: `item: Any` - * - * Used to read the display value from each entry in `items`. - */ - getItemValue: PropTypes.func.isRequired, - /** - * Arguments: `item: Any, isHighlighted: Boolean, styles: Object` - * - * Invoked for each entry in `items` that also passes `shouldItemRender` to - * generate the render tree for each item in the dropdown menu. `styles` is - * an optional set of styles that can be applied to improve the look/feel - * of the items in the dropdown menu. - */ - renderItem: PropTypes.func.isRequired, - /** - * Arguments: `items: Array, value: String, styles: Object` - * - * Invoked to generate the render tree for the dropdown menu. Ensure the - * returned tree includes every entry in `items` or else the highlight order - * and keyboard navigation logic will break. `styles` will contain - * { top, left, minWidth } which are the coordinates of the top-left corner - * and the width of the dropdown menu. - */ - renderMenu: PropTypes.func, - /** - * Styles that are applied to the dropdown menu in the default `renderMenu` - * implementation. If you override `renderMenu` and you want to use - * `menuStyle` you must manually apply them (`this.props.menuStyle`). - */ - menuStyle: PropTypes.object, - /** - * Arguments: `props: Object` - * - * Invoked to generate the input element. The `props` argument is the result - * of merging `props.inputProps` with a selection of props that are required - * both for functionality and accessibility. At the very least you need to - * apply `props.ref` and all `props.on` event handlers. Failing to do - * this will cause `Autocomplete` to behave unexpectedly. - */ - renderInput: PropTypes.func, - /** - * Props passed to `props.renderInput`. By default these props will be - * applied to the `` element rendered by `Autocomplete`, unless you - * have specified a custom value for `props.renderInput`. Any properties - * supported by `HTMLInputElement` can be specified, apart from the - * following which are set by `Autocomplete`: value, autoComplete, role, - * aria-autocomplete. `inputProps` is commonly used for (but not limited to) - * placeholder, event handlers (onFocus, onBlur, etc.), autoFocus, etc.. - */ - inputProps: PropTypes.object, - /** - * Props that are applied to the element which wraps the `` and - * dropdown menu elements rendered by `Autocomplete`. - */ - wrapperProps: PropTypes.object, - /** - * This is a shorthand for `wrapperProps={{ style: }}`. - * Note that `wrapperStyle` is applied before `wrapperProps`, so the latter - * will win if it contains a `style` entry. - */ - wrapperStyle: PropTypes.object, - /** - * Whether or not to automatically highlight the top match in the dropdown - * menu. - */ - autoHighlight: PropTypes.bool, - /** - * Whether or not to automatically select the highlighted item when the - * `` loses focus. - */ - selectOnBlur: PropTypes.bool, - /** - * Arguments: `isOpen: Boolean` - * - * Invoked every time the dropdown menu's visibility changes (i.e. every - * time it is displayed/hidden). - */ - onMenuVisibilityChange: PropTypes.func, - /** - * Used to override the internal logic which displays/hides the dropdown - * menu. This is useful if you want to force a certain state based on your - * UX/business logic. Use it together with `onMenuVisibilityChange` for - * fine-grained control over the dropdown menu dynamics. - */ - open: PropTypes.bool, - debug: PropTypes.bool, - }; - - static defaultProps = { - value: '', - wrapperProps: {}, - wrapperStyle: { - display: 'inline-block', - }, - inputProps: {}, - renderInput(props) { - return ; - }, - onChange() {}, - onSelect() {}, - renderMenu(items, value, style) { - return
; - }, - menuStyle: { - borderRadius: '3px', - boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)', - background: 'rgba(255, 255, 255, 0.9)', - padding: '2px 0', - fontSize: '90%', - position: 'fixed', - overflow: 'auto', - maxHeight: '50%', // TODO: don't cheat, let it flow to the bottom, - }, - autoHighlight: true, - selectOnBlur: false, - onMenuVisibilityChange() {}, - }; - - constructor(props) { - super(props); - this.state = { - isOpen: false, - highlightedIndex: null, - }; - this._debugStates = []; - this.ensureHighlightedIndex = this.ensureHighlightedIndex.bind(this); - this.exposeAPI = this.exposeAPI.bind(this); - this.handleInputFocus = this.handleInputFocus.bind(this); - this.handleInputBlur = this.handleInputBlur.bind(this); - this.handleChange = this.handleChange.bind(this); - this.handleKeyDown = this.handleKeyDown.bind(this); - this.handleInputClick = this.handleInputClick.bind(this); - this.maybeAutoCompleteText = this.maybeAutoCompleteText.bind(this); - } - - componentWillMount() { - // this.refs is frozen, so we need to assign a new object to it - this.refs = {}; - this._ignoreBlur = false; - this._ignoreFocus = false; - this._scrollOffset = null; - this._scrollTimer = null; - } - - componentWillUnmount() { - clearTimeout(this._scrollTimer); - this._scrollTimer = null; - } - - componentWillReceiveProps(nextProps) { - if (this.state.highlightedIndex !== null) { - this.setState(this.ensureHighlightedIndex); - } - if ( - nextProps.autoHighlight && - (this.props.value !== nextProps.value || this.state.highlightedIndex === null) - ) { - this.setState(this.maybeAutoCompleteText); - } - } - - componentDidMount() { - if (this.isOpen()) { - this.setMenuPositions(); - } - } - - componentDidUpdate(prevProps, prevState) { - if ( - (this.state.isOpen && !prevState.isOpen) || - ('open' in this.props && this.props.open && !prevProps.open) - ) - this.setMenuPositions(); - - this.maybeScrollItemIntoView(); - if (prevState.isOpen !== this.state.isOpen) { - this.props.onMenuVisibilityChange(this.state.isOpen); - } - } - - exposeAPI(el) { - this.refs.input = el; - IMPERATIVE_API.forEach(ev => (this[ev] = el && el[ev] && el[ev].bind(el))); - } - - maybeScrollItemIntoView() { - if (this.isOpen() && this.state.highlightedIndex !== null) { - const itemNode = this.refs[`item-${this.state.highlightedIndex}`]; - const menuNode = this.refs.menu; - scrollIntoView(findDOMNode(itemNode), findDOMNode(menuNode), { - onlyScrollIfNeeded: true, - }); - } - } - - handleKeyDown(event) { - if (Autocomplete.keyDownHandlers[event.key]) - Autocomplete.keyDownHandlers[event.key].call(this, event); - else if (!this.isOpen()) { - this.setState({ - isOpen: true, - }); - } - } - - handleChange(event) { - this.props.onChange(event, event.target.value); - } - - static keyDownHandlers = { - ArrowDown(event) { - event.preventDefault(); - const itemsLength = this.getFilteredItems(this.props).length; - if (!itemsLength) return; - const { highlightedIndex } = this.state; - const index = - highlightedIndex === null || highlightedIndex === itemsLength - 1 - ? 0 - : highlightedIndex + 1; - this.setState({ - highlightedIndex: index, - isOpen: true, - }); - }, - - ArrowUp(event) { - event.preventDefault(); - const itemsLength = this.getFilteredItems(this.props).length; - if (!itemsLength) return; - const { highlightedIndex } = this.state; - const index = - highlightedIndex === 0 || highlightedIndex === null - ? itemsLength - 1 - : highlightedIndex - 1; - this.setState({ - highlightedIndex: index, - isOpen: true, - }); - }, - - Enter(event) { - // Key code 229 is used for selecting items from character selectors (Pinyin, Kana, etc) - if (event.keyCode !== 13) return; - if (!this.isOpen()) { - // menu is closed so there is no selection to accept -> do nothing - } else if (this.state.highlightedIndex == null) { - // input has focus but no menu item is selected + enter is hit -> close the menu, highlight whatever's in input - this.setState( - { - isOpen: false, - }, - () => { - this.refs.input.select(); - } - ); - } else { - // text entered + menu item has been highlighted + enter is hit -> update value to that of selected menu item, close the menu - event.preventDefault(); - const item = this.getFilteredItems(this.props)[this.state.highlightedIndex]; - const value = this.props.getItemValue(item); - this.setState( - { - isOpen: false, - highlightedIndex: null, - }, - () => { - // this.refs.input.focus() // TODO: file issue - this.refs.input.setSelectionRange(value.length, value.length); - this.props.onSelect(value, item); - } - ); - } - }, - - Escape() { - // In case the user is currently hovering over the menu - this.setIgnoreBlur(false); - this.setState({ - highlightedIndex: null, - isOpen: false, - }); - }, - - Tab() { - // In case the user is currently hovering over the menu - this.setIgnoreBlur(false); - }, - }; - - getFilteredItems(props) { - let items = props.items; - - if (props.shouldItemRender) { - items = items.filter(item => props.shouldItemRender(item, props.value)); - } - - if (props.sortItems) { - items.sort((a, b) => props.sortItems(a, b, props.value)); - } - - return items; - } - - maybeAutoCompleteText(state, props) { - const { highlightedIndex } = state; - const { value, getItemValue } = props; - const index = highlightedIndex === null ? 0 : highlightedIndex; - const matchedItem = this.getFilteredItems(props)[index]; - if (value !== '' && matchedItem) { - const itemValue = getItemValue(matchedItem); - const itemValueDoesMatch = - itemValue.toLowerCase().indexOf( - value.toLowerCase() - // below line is the the only thing that is changed from the real component - ) !== -1; - if (itemValueDoesMatch) { - return { highlightedIndex: index }; - } - } - return { highlightedIndex: null }; - } - - ensureHighlightedIndex(state, props) { - if (state.highlightedIndex >= this.getFilteredItems(props).length) { - return { highlightedIndex: null }; - } - } - - setMenuPositions() { - const node = this.refs.input; - const rect = node.getBoundingClientRect(); - const computedStyle = global.window.getComputedStyle(node); - const marginBottom = parseInt(computedStyle.marginBottom, 10) || 0; - const marginLeft = parseInt(computedStyle.marginLeft, 10) || 0; - const marginRight = parseInt(computedStyle.marginRight, 10) || 0; - this.setState({ - menuTop: rect.bottom + marginBottom, - menuLeft: rect.left + marginLeft, - menuWidth: rect.width + marginLeft + marginRight, - }); - } - - highlightItemFromMouse(index) { - this.setState({ highlightedIndex: index }); - } - - selectItemFromMouse(item) { - const value = this.props.getItemValue(item); - // The menu will de-render before a mouseLeave event - // happens. Clear the flag to release control over focus - this.setIgnoreBlur(false); - this.setState( - { - isOpen: false, - highlightedIndex: null, - }, - () => { - this.props.onSelect(value, item); - } - ); - } - - setIgnoreBlur(ignore) { - this._ignoreBlur = ignore; - } - - renderMenu() { - const items = this.getFilteredItems(this.props).map((item, index) => { - const element = this.props.renderItem(item, this.state.highlightedIndex === index, { - cursor: 'default', - }); - return React.cloneElement(element, { - onMouseEnter: () => this.highlightItemFromMouse(index), - onClick: () => this.selectItemFromMouse(item), - ref: e => (this.refs[`item-${index}`] = e), - }); - }); - const style = { - left: this.state.menuLeft, - top: this.state.menuTop, - minWidth: this.state.menuWidth, - }; - const menu = this.props.renderMenu(items, this.props.value, style); - return React.cloneElement(menu, { - ref: e => (this.refs.menu = e), - // Ignore blur to prevent menu from de-rendering before we can process click - onMouseEnter: () => this.setIgnoreBlur(true), - onMouseLeave: () => this.setIgnoreBlur(false), - }); - } - - handleInputBlur(event) { - if (this._ignoreBlur) { - this._ignoreFocus = true; - this._scrollOffset = getScrollOffset(); - this.refs.input.focus(); - return; - } - let setStateCallback; - const { highlightedIndex } = this.state; - if (this.props.selectOnBlur && highlightedIndex !== null) { - const items = this.getFilteredItems(this.props); - const item = items[highlightedIndex]; - const value = this.props.getItemValue(item); - setStateCallback = () => this.props.onSelect(value, item); - } - this.setState( - { - isOpen: false, - highlightedIndex: null, - }, - setStateCallback - ); - const { onBlur } = this.props.inputProps; - if (onBlur) { - onBlur(event); - } - } - - handleInputFocus(event) { - if (this._ignoreFocus) { - this._ignoreFocus = false; - const { x, y } = this._scrollOffset; - this._scrollOffset = null; - // Focus will cause the browser to scroll the into view. - // This can cause the mouse coords to change, which in turn - // could cause a new highlight to happen, cancelling the click - // event (when selecting with the mouse) - window.scrollTo(x, y); - // Some browsers wait until all focus event handlers have been - // processed before scrolling the into view, so let's - // scroll again on the next tick to ensure we're back to where - // the user was before focus was lost. We could do the deferred - // scroll only, but that causes a jarring split second jump in - // some browsers that scroll before the focus event handlers - // are triggered. - clearTimeout(this._scrollTimer); - this._scrollTimer = setTimeout(() => { - this._scrollTimer = null; - window.scrollTo(x, y); - }, 0); - return; - } - this.setState({ isOpen: true }); - const { onFocus } = this.props.inputProps; - if (onFocus) { - onFocus(event); - } - } - - isInputFocused() { - const el = this.refs.input; - return el.ownerDocument && el === el.ownerDocument.activeElement; - } - - handleInputClick() { - // Input will not be focused if it's disabled - if (this.isInputFocused() && !this.isOpen()) this.setState({ isOpen: true }); - } - - composeEventHandlers(internal, external) { - return external - ? e => { - internal(e); - external(e); - } - : internal; - } - - isOpen() { - return 'open' in this.props ? this.props.open : this.state.isOpen; - } - - render() { - if (this.props.debug) { - // you don't like it, you love it - this._debugStates.push({ - id: this._debugStates.length, - state: this.state, - }); - } - - const { inputProps, items } = this.props; - - const open = this.isOpen(); - return ( -
- {this.props.renderInput({ - ...inputProps, - role: 'combobox', - 'aria-autocomplete': 'list', - 'aria-expanded': open, - autoComplete: 'off', - ref: this.exposeAPI, - onFocus: this.handleInputFocus, - onBlur: this.handleInputBlur, - onChange: this.handleChange, - onKeyDown: this.composeEventHandlers(this.handleKeyDown, inputProps.onKeyDown), - onClick: this.composeEventHandlers(this.handleInputClick, inputProps.onClick), - value: this.props.value, - })} - {open && !!items.length && this.renderMenu()} - {this.props.debug && ( -
-            {JSON.stringify(
-              this._debugStates.slice(
-                Math.max(0, this._debugStates.length - 5),
-                this._debugStates.length
-              ),
-              null,
-              2
-            )}
-          
- )} -
- ); - } -} diff --git a/src/renderer/component/wunderbar/view.jsx b/src/renderer/component/wunderbar/view.jsx index 27fdd5e17..f332a11ec 100644 --- a/src/renderer/component/wunderbar/view.jsx +++ b/src/renderer/component/wunderbar/view.jsx @@ -1,116 +1,166 @@ -// @flow import React from 'react'; -import lbryuri from 'lbryuri'; -import classnames from 'classnames'; -import Autocomplete from './internal/autocomplete'; +import PropTypes from 'prop-types'; +import lbryuri from 'lbryuri.js'; +import Icon from 'component/icon'; +import { parseQueryParams } from 'util/query_params'; -type Props = { - updateSearchQuery: string => void, - getSearchSuggestions: string => void, - onSearch: string => void, - onSubmit: string => void, - searchQuery: ?string, - isActive: boolean, - address: ?string, - suggestions: Array, -}; +class WunderBar extends React.PureComponent { + static TYPING_TIMEOUT = 800; -class WunderBar extends React.PureComponent { - constructor() { - super(); - (this: any).handleSubmit = this.handleSubmit.bind(this); - (this: any).handleChange = this.handleChange.bind(this); - (this: any).focus = this.focus.bind(this); - this.input = undefined; + static propTypes = { + onSearch: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + this._userTypingTimer = null; + this._isSearchDispatchPending = false; + this._input = null; + this._stateBeforeSearch = null; + this._resetOnNextBlur = true; + this.onChange = this.onChange.bind(this); + this.onFocus = this.onFocus.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onKeyPress = this.onKeyPress.bind(this); + this.onReceiveRef = this.onReceiveRef.bind(this); + this.state = { + address: this.props.address, + icon: this.props.icon, + }; } - input: ?HTMLInputElement; - - handleChange(e: SyntheticInputEvent<*>) { - const { updateSearchQuery, getSearchSuggestions } = this.props; - const { value } = e.target; - - updateSearchQuery(value); - getSearchSuggestions(value); - } - - focus() { - const { input } = this; - if (input) { - input.focus(); + componentWillUnmount() { + if (this.userTypingTimer) { + clearTimeout(this._userTypingTimer); } } - handleSubmit(value: string) { - if (!value) { - return; + onChange(event) { + if (this._userTypingTimer) { + clearTimeout(this._userTypingTimer); } - const { onSubmit, onSearch } = this.props; + this.setState({ address: event.target.value }); - // if they choose the "search for {value}" in the suggestions - // it will contain the {query}?search - const choseDoSuggestedSearch = value.endsWith('?search'); + this._isSearchDispatchPending = true; - let searchValue = value; - if (choseDoSuggestedSearch) { - searchValue = value.slice(0, -7); // trim off ?search + const searchQuery = event.target.value; + + this._userTypingTimer = setTimeout(() => { + const hasQuery = searchQuery.length === 0; + this._resetOnNextBlur = hasQuery; + this._isSearchDispatchPending = false; + if (searchQuery) { + this.props.onSearch(searchQuery.trim()); + } + }, WunderBar.TYPING_TIMEOUT); // 800ms delay, tweak for faster/slower + } + + componentWillReceiveProps(nextProps) { + if ( + nextProps.viewingPage !== this.props.viewingPage || + nextProps.address != this.props.address + ) { + this.setState({ address: nextProps.address, icon: nextProps.icon }); } + } - if (this.input) { - this.input.blur(); - } + onFocus() { + this._stateBeforeSearch = this.state; + const newState = { + icon: 'icon-search', + isActive: true, + }; - try { - const uri = lbryuri.normalize(value); - onSubmit(uri); - } catch (e) { - // search query isn't a valid uri - onSearch(searchValue); + this._focusPending = true; + // below is hacking, improved when we have proper routing + if (!this.state.address.startsWith('lbry://') && this.state.icon !== 'icon-search') { + // onFocus, if they are not on an exact URL or a search page, clear the bar + newState.address = ''; } + this.setState(newState); + } + + onBlur() { + if (this._isSearchDispatchPending) { + setTimeout(() => { + this.onBlur(); + }, WunderBar.TYPING_TIMEOUT + 1); + } else { + const commonState = { isActive: false }; + if (this._resetOnNextBlur) { + this.setState(Object.assign({}, this._stateBeforeSearch, commonState)); + this._input.value = this.state.address; + } else { + this._resetOnNextBlur = true; + this._stateBeforeSearch = this.state; + this.setState(commonState); + } + } + } + + componentDidUpdate() { + if (this._input) { + const start = this._input.selectionStart, + end = this._input.selectionEnd; + + this._input.value = this.state.address; // this causes cursor to go to end of input + + this._input.setSelectionRange(start, end); + + if (this._focusPending) { + this._input.select(); + this._focusPending = false; + } + } + } + + onKeyPress(event) { + if (event.charCode == 13 && this._input.value) { + let uri = null, + method = 'onSubmit', + extraParams = {}; + + this._resetOnNextBlur = false; + clearTimeout(this._userTypingTimer); + + const parts = this._input.value.trim().split('?'); + const value = parts.shift(); + if (parts.length > 0) extraParams = parseQueryParams(parts.join('')); + + try { + uri = lbryuri.normalize(value); + this.setState({ value: uri }); + } catch (error) { + // then it's not a valid URL, so let's search + uri = value; + method = 'onSearch'; + } + + this.props[method](uri, extraParams); + this._input.blur(); + } + } + + onReceiveRef(ref) { + this._input = ref; } render() { - const { searchQuery, isActive, address, suggestions } = this.props; - - // if we are on the file/channel page - // use the address in the history stack - const wunderbarValue = isActive ? searchQuery : searchQuery || address; - return ( -
- { - this.input = ref; - }} - wrapperStyle={{ flex: 1, minHeight: 0 }} - value={wunderbarValue} - items={suggestions} - getItemValue={item => item.value} - onChange={this.handleChange} - onSelect={this.handleSubmit} - renderInput={props => ( - - )} - renderItem={(item, isHighlighted) => ( -
- {item.label} -
- )} +
+ {this.state.icon ? : ''} +
); diff --git a/src/renderer/constants/action_types.js b/src/renderer/constants/action_types.js index 9a8c57c7f..3c828f5cb 100644 --- a/src/renderer/constants/action_types.js +++ b/src/renderer/constants/action_types.js @@ -88,13 +88,9 @@ export const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED'; export const FILE_DELETE = 'FILE_DELETE'; // Search -export const SEARCH_START = 'SEARCH_START'; -export const SEARCH_SUCCESS = 'SEARCH_SUCCESS'; -export const SEARCH_FAIL = 'SEARCH_FAIL'; -export const UPDATE_SEARCH_QUERY = 'UPDATE_SEARCH_QUERY'; -export const SEARCH_SUGGESTIONS_START = 'SEARCH_SUGGESTIONS_START'; -export const GET_SEARCH_SUGGESTIONS_SUCCESS = 'GET_SEARCH_SUGGESTIONS_SUCCESS'; -export const GET_SEARCH_SUGGESTIONS_FAIL = 'GET_SEARCH_SUGGESTIONS_FAIL'; +export const SEARCH_STARTED = 'SEARCH_STARTED'; +export const SEARCH_COMPLETED = 'SEARCH_COMPLETED'; +export const SEARCH_CANCELLED = 'SEARCH_CANCELLED'; // Settings export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED'; diff --git a/flow-typed/reselect.js b/src/renderer/flow-typed/reselect.js similarity index 100% rename from flow-typed/reselect.js rename to src/renderer/flow-typed/reselect.js diff --git a/src/renderer/index.js b/src/renderer/index.js index ad2db3927..fd6dcc1f7 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -31,7 +31,7 @@ ipcRenderer.on('open-uri-requested', (event, uri, newSession) => { try { verification = JSON.parse(atob(uri.substring(15))); } catch (error) { - console.log(error); // eslint-disable-line no-console + console.log(error); } if (verification.token && verification.recaptcha) { app.store.dispatch(doConditionalAuthNavigate(newSession)); @@ -112,10 +112,10 @@ const init = () => { ReactDOM.render( - +
- +
, document.getElementById('app') ); diff --git a/src/renderer/page/channel/view.jsx b/src/renderer/page/channel/view.jsx index 7ce61b182..f32a9f938 100644 --- a/src/renderer/page/channel/view.jsx +++ b/src/renderer/page/channel/view.jsx @@ -1,4 +1,3 @@ -/* eslint-disable */ import React from 'react'; import lbryuri from 'lbryuri'; import { BusyMessage } from 'component/common'; @@ -6,7 +5,6 @@ import FileTile from 'component/fileTile'; import ReactPaginate from 'react-paginate'; import Link from 'component/link'; import SubscribeButton from 'component/subscribeButton'; -import Page from 'component/page'; class ChannelPage extends React.PureComponent { componentDidMount() { @@ -72,7 +70,7 @@ class ChannelPage extends React.PureComponent { } return ( - +
@@ -109,10 +107,9 @@ class ChannelPage extends React.PureComponent { containerClassName="pagination" /> )} - +
); } } export default ChannelPage; -/* eslint-enable */ diff --git a/src/renderer/page/discover/view.jsx b/src/renderer/page/discover/view.jsx index 28adf330c..52b841bab 100644 --- a/src/renderer/page/discover/view.jsx +++ b/src/renderer/page/discover/view.jsx @@ -1,37 +1,259 @@ -// @flow import React from 'react'; -import Page from 'component/page'; -import CategoryList from 'component/common/category-list'; +import ReactDOM from 'react-dom'; +import lbryuri from 'lbryuri'; +import FileCard from 'component/fileCard'; +import { BusyMessage } from 'component/common.js'; +import Icon from 'component/icon'; +import ToolTip from 'component/tooltip.js'; +import SubHeader from 'component/subHeader'; +import classnames from 'classnames'; +import Link from 'component/link'; -type Props = { - fetchFeaturedUris: () => void, - fetchingFeaturedUris: boolean, - featuredUris: {}, -}; +// This should be in a separate file +export class FeaturedCategory extends React.PureComponent { + constructor() { + super(); -class DiscoverPage extends React.PureComponent { + this.state = { + numItems: undefined, + canScrollPrevious: false, + canScrollNext: false, + }; + } + + componentWillMount() { + this.setState({ + numItems: this.props.names.length, + }); + } + + componentDidMount() { + const cardRow = ReactDOM.findDOMNode(this.refs.rowitems); + const cards = cardRow.getElementsByTagName('section'); + + // check if the last card is visible + const lastCard = cards[cards.length - 1]; + const isCompletelyVisible = this.isCardVisible(lastCard, cardRow, false); + + if (!isCompletelyVisible) { + this.setState({ + canScrollNext: true, + }); + } + } + + handleScrollPrevious() { + const cardRow = ReactDOM.findDOMNode(this.refs.rowitems); + if (cardRow.scrollLeft > 0) { + // check the visible cards + const cards = cardRow.getElementsByTagName('section'); + let firstVisibleCard = null; + let firstVisibleIdx = -1; + for (let i = 0; i < cards.length; i++) { + if (this.isCardVisible(cards[i], cardRow, false)) { + firstVisibleCard = cards[i]; + firstVisibleIdx = i; + break; + } + } + + const numDisplayed = this.numDisplayedCards(cardRow); + const scrollToIdx = firstVisibleIdx - numDisplayed; + const animationCallback = () => { + this.setState({ + canScrollPrevious: cardRow.scrollLeft !== 0, + canScrollNext: true, + }); + }; + this.scrollCardItemsLeftAnimated( + cardRow, + scrollToIdx < 0 ? 0 : cards[scrollToIdx].offsetLeft, + 100, + animationCallback + ); + } + } + + handleScrollNext() { + const cardRow = ReactDOM.findDOMNode(this.refs.rowitems); + + // check the visible cards + const cards = cardRow.getElementsByTagName('section'); + let lastVisibleCard = null; + let lastVisibleIdx = -1; + for (let i = 0; i < cards.length; i++) { + if (this.isCardVisible(cards[i], cardRow, true)) { + lastVisibleCard = cards[i]; + lastVisibleIdx = i; + } + } + + if (lastVisibleCard) { + const numDisplayed = this.numDisplayedCards(cardRow); + const animationCallback = () => { + // update last visible index after scroll + for (let i = 0; i < cards.length; i++) { + if (this.isCardVisible(cards[i], cardRow, true)) { + lastVisibleIdx = i; + } + } + + this.setState({ canScrollPrevious: true }); + if (lastVisibleIdx === cards.length - 1) { + this.setState({ canScrollNext: false }); + } + }; + + this.scrollCardItemsLeftAnimated( + cardRow, + Math.min(lastVisibleCard.offsetLeft, cardRow.scrollWidth - cardRow.clientWidth), + 100, + animationCallback + ); + } + } + + scrollCardItemsLeftAnimated(cardRow, target, duration, callback) { + if (!duration || duration <= diff) { + cardRow.scrollLeft = target; + if (callback) { + callback(); + } + return; + } + + const component = this; + const diff = target - cardRow.scrollLeft; + const tick = diff / duration * 10; + setTimeout(() => { + cardRow.scrollLeft += tick; + if (cardRow.scrollLeft === target) { + if (callback) { + callback(); + } + return; + } + component.scrollCardItemsLeftAnimated(cardRow, target, duration - 10, callback); + }, 10); + } + + isCardVisible(section, cardRow, partialVisibility) { + // check if a card is fully or partialy visible in its parent + const cardRowWidth = cardRow.offsetWidth; + const cardRowLeft = cardRow.scrollLeft; + const cardRowEnd = cardRowLeft + cardRow.offsetWidth; + const sectionLeft = section.offsetLeft - cardRowLeft; + const sectionEnd = sectionLeft + section.offsetWidth; + + return ( + (sectionLeft >= 0 && sectionEnd <= cardRowWidth) || + (((sectionLeft < 0 && sectionEnd > 0) || (sectionLeft > 0 && sectionLeft <= cardRowWidth)) && + partialVisibility) + ); + } + + numDisplayedCards(cardRow) { + const cards = cardRow.getElementsByTagName('section'); + const cardRowWidth = cardRow.offsetWidth; + // get the width of the first card and then calculate + const cardWidth = cards.length > 0 ? cards[0].offsetWidth : 0; + + if (cardWidth > 0) { + return Math.ceil(cardRowWidth / cardWidth); + } + + // return a default value of 1 card displayed if the card width couldn't be determined + return 1; + } + + render() { + const { category, names, categoryLink } = this.props; + + return ( +
+

+ {categoryLink ? ( + + ) : ( + category + )} + + {category && + category.match(/^community/i) && ( + + )} +

+
+ {this.state.canScrollPrevious && ( +
+ + + +
+ )} + {this.state.canScrollNext && ( +
+ + + +
+ )} +
+ {names && + names.map(name => ( + + ))} +
+
+
+ ); + } +} + +class DiscoverPage extends React.PureComponent { componentWillMount() { this.props.fetchFeaturedUris(); } render() { const { featuredUris, fetchingFeaturedUris } = this.props; - const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length; - const failedToLoad = !fetchingFeaturedUris && !hasContent; + const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length, + failedToLoad = !fetchingFeaturedUris && !hasContent; return ( - +
+ + {!hasContent && fetchingFeaturedUris && } {hasContent && Object.keys(featuredUris).map( category => featuredUris[category].length ? ( - + ) : ( '' ) )} {failedToLoad &&
{__('Failed to load landing content.')}
} - +
); } } diff --git a/src/renderer/page/file/view.jsx b/src/renderer/page/file/view.jsx index d9a224c84..fb8f741c8 100644 --- a/src/renderer/page/file/view.jsx +++ b/src/renderer/page/file/view.jsx @@ -1,4 +1,3 @@ -/* eslint-disable */ import React from 'react'; import lbry from 'lbry'; import lbryuri from 'lbryuri'; @@ -7,13 +6,12 @@ import { Thumbnail } from 'component/common'; import FilePrice from 'component/filePrice'; import FileDetails from 'component/fileDetails'; import UriIndicator from 'component/uriIndicator'; -import Icon from 'component/common/icon'; +import Icon from 'component/icon'; import WalletSendTip from 'component/walletSendTip'; import DateTime from 'component/dateTime'; import * as icons from 'constants/icons'; import Link from 'component/link'; import SubscribeButton from 'component/subscribeButton'; -import Page from 'component/page'; class FilePage extends React.PureComponent { componentDidMount() { @@ -71,52 +69,49 @@ class FilePage extends React.PureComponent { } return ( - -
-
- {isPlayable ? ( -
-
- {(!tab || tab === 'details') && ( -
- {' '} -
- {!fileInfo || fileInfo.written_bytes <= 0 ? ( - - - {isRewardContent && ( - - {' '} - - - )} - - ) : null} -

{title}

-
- - - Published on - -
+
+
+ {isPlayable ? ( +
+
+ {(!tab || tab === 'details') && ( +
+ {' '} +
+ {!fileInfo || fileInfo.written_bytes <= 0 ? ( + + + {isRewardContent && ( + + {' '} + + + )} + + ) : null} +

{title}

+
+ + + Published on +
- -
- )} - {tab === 'tip' && } -
-
- + + +
+ )} + {tab === 'tip' && } +
+
); } } export default FilePage; -/* eslint-enable */ diff --git a/src/renderer/page/help/view.jsx b/src/renderer/page/help/view.jsx index c2590fd81..bb22e6fe5 100644 --- a/src/renderer/page/help/view.jsx +++ b/src/renderer/page/help/view.jsx @@ -4,7 +4,7 @@ import lbry from 'lbry.js'; import Link from 'component/link'; import SubHeader from 'component/subHeader'; import { BusyMessage } from 'component/common'; -import Icon from 'component/common/icon'; +import Icon from 'component/icon'; class HelpPage extends React.PureComponent { constructor(props) { diff --git a/src/renderer/page/search/view.jsx b/src/renderer/page/search/view.jsx index c3cd0cc7e..48aed6fce 100644 --- a/src/renderer/page/search/view.jsx +++ b/src/renderer/page/search/view.jsx @@ -1,21 +1,15 @@ -// @flow import React from 'react'; import lbryuri from 'lbryuri'; import FileTile from 'component/fileTile'; import FileListSearch from 'component/fileListSearch'; -import ToolTip from 'component/common/tooltip'; -import Page from 'component/page'; +import { ToolTip } from 'component/tooltip.js'; -type Props = { - query: ?string, -}; - -class SearchPage extends React.PureComponent { +class SearchPage extends React.PureComponent { render() { const { query } = this.props; return ( - +
{lbryuri.isValid(query) ? (

@@ -42,7 +36,7 @@ class SearchPage extends React.PureComponent {

- +
); } } diff --git a/src/renderer/page/show/view.jsx b/src/renderer/page/show/view.jsx index 60a1fd06f..ebb45da30 100644 --- a/src/renderer/page/show/view.jsx +++ b/src/renderer/page/show/view.jsx @@ -1,24 +1,17 @@ -/* eslint-disable */ import React from 'react'; +import lbryuri from 'lbryuri'; import { BusyMessage } from 'component/common'; import ChannelPage from 'page/channel'; import FilePage from 'page/file'; -type Props = { - isResolvingUri: boolean, - resolveUri: string => void, - uri: string, - claim: { name: string }, -}; - -class ShowPage extends React.PureComponent { +class ShowPage extends React.PureComponent { componentWillMount() { const { isResolvingUri, resolveUri, uri } = this.props; if (!isResolvingUri) resolveUri(uri); } - componentWillReceiveProps(nextProps: Props) { + componentWillReceiveProps(nextProps) { const { isResolvingUri, resolveUri, claim, uri } = nextProps; if (!isResolvingUri && claim === undefined && uri) { @@ -54,9 +47,8 @@ class ShowPage extends React.PureComponent { innerContent = ; } - return innerContent; + return
{innerContent}
; } } export default ShowPage; -/* eslint-enable */ diff --git a/src/renderer/page/subscriptions/view.jsx b/src/renderer/page/subscriptions/view.jsx index 3c4e6e61a..3e09748a5 100644 --- a/src/renderer/page/subscriptions/view.jsx +++ b/src/renderer/page/subscriptions/view.jsx @@ -1,8 +1,8 @@ // @flow import React from 'react'; import SubHeader from 'component/subHeader'; -import { BusyMessage } from 'component/common'; -import CategoryList from 'component/common/category-list'; +import { BusyMessage } from 'component/common.js'; +import { FeaturedCategory } from 'page/discover/view'; import type { Subscription } from 'redux/reducers/subscriptions'; type SavedSubscriptions = Array; @@ -83,7 +83,7 @@ export default class extends React.PureComponent { } return ( - - searchResponse.status === 200 - ? Promise.resolve(searchResponse.json()) - : Promise.reject(new Error(searchResponse.statusText)); +// eslint-disable-next-line import/prefer-default-export +export function doSearch(rawQuery) { + return (dispatch, getState) => { + const state = getState(); + const page = selectCurrentPage(state); -export const doSearch = rawQuery => (dispatch, getState) => { - const state = getState(); - const page = selectCurrentPage(state); + const query = rawQuery.replace(/^lbry:\/\//i, ''); - const query = rawQuery.replace(/^lbry:\/\//i, ''); - - if (!query) { - dispatch({ - type: ACTIONS.SEARCH_FAIL, - }); - return; - } - - dispatch({ - type: ACTIONS.SEARCH_START, - data: { query }, - }); - - if (page !== 'search') { - dispatch(doNavigate('search', { query })); - } else { - fetch(`https://lighthouse.lbry.io/search?s=${query}`) - .then(handleSearchApiResponse) - .then(data => { - const uris = []; - const actions = []; - - data.forEach(result => { - const uri = Lbryuri.build({ - name: result.name, - claimId: result.claimId, - }); - actions.push(doResolveUri(uri)); - uris.push(uri); - }); - - actions.push({ - type: ACTIONS.SEARCH_SUCCESS, - data: { - query, - uris, - }, - }); - dispatch(batchActions(...actions)); - }) - .catch(() => { - dispatch({ - type: ACTIONS.SEARCH_FAIL, - }); - }); - } -}; - -export const updateSearchQuery = searchQuery => ({ - type: ACTIONS.UPDATE_SEARCH_QUERY, - data: { searchQuery }, -}); - -export const getSearchSuggestions = value => dispatch => { - dispatch({ type: ACTIONS.SEARCH_SUGGESTIONS_START }); - if (!value) { - dispatch({ - type: ACTIONS.GET_SEARCH_SUGGESTIONS_SUCCESS, - data: [], - }); - return; - } - - // This should probably be more robust - let searchValue = value; - if (searchValue.startsWith('lbry://')) { - searchValue = searchValue.slice(7); - } - - // need to handle spaces in the query? - fetch(`https://lighthouse.lbry.io/autocomplete?s=${searchValue}`) - .then(handleSearchApiResponse) - .then(suggestions => { - const formattedSuggestions = suggestions.slice(0, 5).map(suggestion => ({ - label: suggestion, - value: suggestion, - })); - - // Should we add lbry://{query} as the first result? - // If it's not a valid uri, then add a "search for {query}" result - const searchLabel = `Search for "${value}"`; - try { - const uri = Lbryuri.normalize(value); - formattedSuggestions.unshift( - { label: uri, value: uri }, - { label: searchLabel, value: `${value}?search` } - ); - } catch (e) { - if (value) { - formattedSuggestions.unshift({ label: searchLabel, value }); - } - } - - return dispatch({ - type: ACTIONS.GET_SEARCH_SUGGESTIONS_SUCCESS, - data: formattedSuggestions, - }); - }) - .catch(err => + if (!query) { dispatch({ - type: ACTIONS.GET_SEARCH_SUGGESTIONS_FAIL, - data: err, - }) - ); -}; + type: ACTIONS.SEARCH_CANCELLED, + }); + return; + } + + dispatch({ + type: ACTIONS.SEARCH_STARTED, + data: { query }, + }); + + if (page !== 'search') { + dispatch(doNavigate('search', { query })); + } else { + fetch(`https://lighthouse.lbry.io/search?s=${query}`) + .then( + response => + response.status === 200 + ? Promise.resolve(response.json()) + : Promise.reject(new Error(response.statusText)) + ) + .then(data => { + const uris = []; + const actions = []; + + data.forEach(result => { + const uri = Lbryuri.build({ + name: result.name, + claimId: result.claimId, + }); + actions.push(doResolveUri(uri)); + uris.push(uri); + }); + + actions.push({ + type: ACTIONS.SEARCH_COMPLETED, + data: { + query, + uris, + }, + }); + dispatch(batchActions(...actions)); + }) + .catch(() => { + dispatch({ + type: ACTIONS.SEARCH_CANCELLED, + }); + }); + } + }; +} diff --git a/src/renderer/redux/reducers/search.js b/src/renderer/redux/reducers/search.js index 3a8330b93..b3a0754e4 100644 --- a/src/renderer/redux/reducers/search.js +++ b/src/renderer/redux/reducers/search.js @@ -1,75 +1,32 @@ -// @flow import * as ACTIONS from 'constants/action_types'; -import { handleActions } from 'util/redux-utils'; - -type SearchState = { - isActive: boolean, - searchQuery: string, - searchingForSuggestions: boolean, - suggestions: Array, - urisByQuery: {}, -}; +const reducers = {}; const defaultState = { - isActive: false, - searchQuery: '', // needs to be an empty string for input focusing - searchingForSuggestions: false, - suggestions: [], urisByQuery: {}, + searching: false, }; -export default handleActions( - { - [ACTIONS.SEARCH_START]: (state: SearchState): SearchState => ({ - ...state, - searching: true, - }), - [ACTIONS.SEARCH_SUCCESS]: (state: SearchState, action): SearchState => { - const { query, uris } = action.data; +reducers[ACTIONS.SEARCH_STARTED] = state => + Object.assign({}, state, { + searching: true, + }); - return { - ...state, - searching: false, - urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }), - }; - }, +reducers[ACTIONS.SEARCH_COMPLETED] = (state, action) => { + const { query, uris } = action.data; - [ACTIONS.SEARCH_FAIL]: (state: SearchState): SearchState => ({ - ...state, - searching: false, - }), + return Object.assign({}, state, { + searching: false, + urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }), + }); +}; - [ACTIONS.UPDATE_SEARCH_QUERY]: (state: SearchState, action): SearchState => ({ - ...state, - searchQuery: action.data.searchQuery, - suggestions: [], - isActive: true, - }), +reducers[ACTIONS.SEARCH_CANCELLED] = state => + Object.assign({}, state, { + searching: false, + }); - [ACTIONS.SEARCH_SUGGESTIONS_START]: (state: SearchState): SearchState => ({ - ...state, - searchingForSuggestions: true, - suggestions: [], - }), - [ACTIONS.GET_SEARCH_SUGGESTIONS_SUCCESS]: (state: SearchState, action): SearchState => ({ - ...state, - searchingForSuggestions: false, - suggestions: action.data, - }), - [ACTIONS.GET_SEARCH_SUGGESTIONS_FAIL]: (state: SearchState): SearchState => ({ - ...state, - searchingForSuggestions: false, - // error, TODO: figure this out on the search page - }), - - // clear the searchQuery on back/forward - // it may be populated by the page title for search/file pages - // if going home, it should be blank - [ACTIONS.HISTORY_NAVIGATE]: (state: SearchState): SearchState => ({ - ...state, - searchQuery: '', - isActive: false, - }), - }, - defaultState -); +export default function reducer(state = defaultState, action) { + const handler = reducers[action.type]; + if (handler) return handler(state, action); + return state; +} diff --git a/src/renderer/redux/selectors/navigation.js b/src/renderer/redux/selectors/navigation.js index 014a0b988..dc0b507a1 100644 --- a/src/renderer/redux/selectors/navigation.js +++ b/src/renderer/redux/selectors/navigation.js @@ -68,6 +68,30 @@ export const selectPageTitle = createSelector( selectCurrentParams, (page, params) => { switch (page) { + case 'settings': + return __('Settings'); + case 'report': + return __('Report'); + case 'wallet': + return __('Wallet'); + case 'send': + return __('Send or Receive LBRY Credits'); + case 'getcredits': + return __('Get LBRY Credits'); + case 'backup': + return __('Backup Your Wallet'); + case 'rewards': + return __('Rewards'); + case 'invite': + return __('Invites'); + case 'start': + return __('Start'); + case 'publish': + return params.id ? __('Edit') : __('Publish'); + case 'help': + return __('Help'); + case 'developer': + return __('Developer'); case 'show': { const parts = [Lbryuri.normalize(params.uri)]; // If the params has any keys other than "uri" @@ -76,14 +100,21 @@ export const selectPageTitle = createSelector( } return parts.join('?'); } + case 'downloaded': + return __('Downloads & Purchases'); + case 'published': + return __('Publications'); + case 'search': + return params.query ? __('Search results for %s', params.query) : __('Search'); + case 'subscriptions': + return __('Your Subscriptions'); case 'discover': - return __('Discover'); case false: case null: case '': return ''; default: - return ''; + return page[0].toUpperCase() + (page.length > 0 ? page.substr(1) : ''); } } ); diff --git a/src/renderer/redux/selectors/search.js b/src/renderer/redux/selectors/search.js index 8eb6c95c0..4e49f47d8 100644 --- a/src/renderer/redux/selectors/search.js +++ b/src/renderer/redux/selectors/search.js @@ -28,15 +28,52 @@ export const selectWunderBarAddress = createSelector( selectCurrentPage, selectPageTitle, selectSearchQuery, - (page, title, query) => { - // only populate the wunderbar address if we are on the file/channel pages - // or show the search query - if (page === 'show') { - return title; - } else if (page === 'search') { - return query; - } + (page, title, query) => (page !== 'search' ? title : query || title) +); - return ''; +export const selectWunderBarIcon = createSelector( + selectCurrentPage, + selectCurrentParams, + (page, params) => { + switch (page) { + case 'auth': + return 'icon-user'; + case 'settings': + return 'icon-gear'; + case 'help': + return 'icon-question'; + case 'report': + return 'icon-file'; + case 'downloaded': + return 'icon-folder'; + case 'published': + return 'icon-folder'; + case 'history': + return 'icon-history'; + case 'send': + return 'icon-send'; + case 'rewards': + return 'icon-rocket'; + case 'invite': + return 'icon-envelope-open'; + case 'getcredits': + return 'icon-shopping-cart'; + case 'wallet': + case 'backup': + return 'icon-bank'; + case 'show': + return 'icon-file'; + case 'publish': + return params.id ? __('icon-pencil') : __('icon-upload'); + case 'developer': + return 'icon-code'; + case 'discover': + case 'search': + return 'icon-search'; + case 'subscriptions': + return 'icon-th-list'; + default: + return 'icon-file'; + } } ); diff --git a/src/renderer/scss/_gui.scss b/src/renderer/scss/_gui.scss index db1eedf76..92b82d146 100644 --- a/src/renderer/scss/_gui.scss +++ b/src/renderer/scss/_gui.scss @@ -1,30 +1,4 @@ -// Generic html styles used accross the App -// component specific styling should go in the component scss file - -// The actual fonts used will change ex: medium vs regular -@font-face { - font-family: 'Metropolis'; - font-weight: normal; - font-style: normal; - text-rendering: optimizeLegibility; - src: url('../../../static/font/metropolis/Metropolis-Medium.woff2') format('woff2'); -} - -@font-face { - font-family: 'Metropolis'; - font-weight: 600; - font-style: normal; - text-rendering: optimizeLegibility; - src: url('../../../static/font/metropolis/Metropolis-SemiBold.woff2') format('woff2'); -} - -@font-face { - font-family: 'Metropolis'; - font-weight: 800; - font-style: normal; - text-rendering: optimizeLegibility; - src: url('../../../static/font/metropolis/Metropolis-ExtraBold.woff2') format('woff2'); -} +@import url(https://fonts.googleapis.com/css?family=Roboto:400,400i,500,500i,700); html { height: 100%; @@ -33,20 +7,84 @@ html { body { color: var(--text-color); - font-family: 'Metropolis', sans-serif; + font-family: 'Roboto', sans-serif; line-height: var(--font-line-height); - height: 100%; - overflow: hidden; } -h1, -h2, -h3, -h4, -h5 { +/* Custom text selection */ +*::selection { + background: var(--text-selection-bg); + color: var(--text-selection-color); +} + +#window { + min-height: 100vh; + background: var(--window-bg); +} + +.credit-amount--indicator { + font-weight: 500; + color: var(--color-money); +} +.credit-amount--fee { + font-size: 0.9em; + color: var(--color-meta-light); +} + +.credit-amount--bold { font-weight: 700; } +#main-content { + margin: auto; + display: flex; + flex-direction: column; + overflow: overlay; + padding: $spacing-vertical; + position: absolute; + top: var(--header-height); + bottom: 0; + left: 4px; + right: 4px; + main { + margin-left: auto; + margin-right: auto; + max-width: 100%; + } + main.main--single-column { + width: $width-page-constrained; + } + + main.main--no-margin { + margin-left: 0; + margin-right: 0; + } +} + +.reloading { + &:before { + $width: 30px; + position: absolute; + background: url('../../../static/img/busy.gif') no-repeat center center; + width: $width; + height: $spacing-vertical; + content: ''; + left: 50%; + margin-left: -1 / 2 * $width; + display: inline-block; + } +} + +.icon-fixed-width { + /* This borrowed is from a component of Font Awesome we're not using, maybe add it? */ + width: (18em / 14); + text-align: center; +} + +.icon--left-pad { + padding-left: 3px; +} + h2 { font-size: 1.75em; } @@ -62,13 +100,11 @@ h4 { h5 { font-size: 1.1em; } - sup, sub { vertical-align: baseline; position: relative; } - sup { top: -0.4em; } @@ -81,67 +117,11 @@ code { background-color: var(--color-bg-alt); } -// Without this buttons don't have the Metropolis font -button { - font-family: inherit; -} - -#window { - height: 100%; - overflow: hidden; -} - -#main-content { - height: 100%; - overflow-y: auto; - position: absolute; - left: 0px; - right: 0px; - // don't use {bottom/top} here - // they cause flashes of un-rendered content when scrolling - margin-top: var(--header-height); - // TODO: fix this scrollbar extends beyond screen at the bottom - padding-bottom: var(--header-height); - background-color: var(--color-bg); -} - -.main { - padding: 0 $spacing-vertical * 2/3; -} - -.main--no-padding { - padding-left: 0; - padding-right: 0; -} - -.page__header { - padding: $spacing-vertical * 2/3; - padding-bottom: 0; -} - -.page__title { - font-weight: 800; - font-size: 3em; -} - -/* Custom text selection */ -*::selection { - background: var(--text-selection-bg); - color: var(--text-selection-color); -} - -.credit-amount--indicator { - font-weight: 500; - color: var(--color-money); -} - -.credit-amount--fee { - font-size: 0.9em; - color: var(--color-meta-light); -} - -.credit-amount--bold { - font-weight: 700; +p { + margin-bottom: 0.8em; + &:last-child { + margin-bottom: 0; + } } .hidden { @@ -212,3 +192,7 @@ button { section.section-spaced { margin-bottom: $spacing-vertical; } + +.text-center { + text-align: center; +} diff --git a/src/renderer/scss/_icons.scss b/src/renderer/scss/_icons.scss index 7797614a8..c589a1ac5 100644 --- a/src/renderer/scss/_icons.scss +++ b/src/renderer/scss/_icons.scss @@ -27,16 +27,6 @@ transform: translate(0, 0); } -.icon--fixed-width { - /* This borrowed is from a component of Font Awesome we're not using, maybe add it? */ - width: (18em / 14); - text-align: center; -} - -.icon--padded { - padding: 0 3px; -} - /* Adjustments for icon size and alignment */ .icon-rocket { color: orangered; diff --git a/src/renderer/scss/_vars.scss b/src/renderer/scss/_vars.scss index d93fd2217..15436a5b1 100644 --- a/src/renderer/scss/_vars.scss +++ b/src/renderer/scss/_vars.scss @@ -6,8 +6,6 @@ $width-page-constrained: 800px; $text-color: #000; :root { - --spacing-vertical: 24px; - /* Colors */ --color-brand: #155b4a; --color-primary: #155b4a; @@ -23,8 +21,7 @@ $text-color: #000; --color-download: rgba(0, 0, 0, 0.75); --color-canvas: #f5f5f5; --color-bg: #ffffff; - --color-bg-alt: #f6f6f6; - --color-placeholder: #ececec; + --color-bg-alt: #d9d9d9; /* Misc */ --content-max-width: 1000px; @@ -37,7 +34,7 @@ $text-color: #000; --font-size-subtext-multiple: 0.82; /* Shadows */ - --box-shadow-layer: 0px 1px 3px 0px rgba(0, 0, 0, 0.2); + --box-shadow-layer: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); --box-shadow-focus: 2px 4px 4px 0 rgba(0, 0, 0, 0.14), 2px 5px 3px -2px rgba(0, 0, 0, 0.2), 2px 3px 7px 0 rgba(0, 0, 0, 0.12); @@ -53,6 +50,9 @@ $text-color: #000; --text-selection-bg: rgba(saturate(lighten(#155b4a, 20%), 20%), 1); // temp color --text-selection-color: #fff; + /* Window */ + --window-bg: var(--color-canvas); + /* Form */ --form-label-color: rgba(0, 0, 0, 0.54); @@ -80,23 +80,21 @@ $text-color: #000; --select-bg: var(--color-bg-alt); --select-color: var(--text-color); - //TODO: determine proper button variables; /* Button */ - --btn-primary-color: #fff; - --button-alt-color: var(--text-color); - --btn-primary-bg: var(--color-primary); - --btn-alt-bg: red; - --btn-radius: 10px; - // below needed? - --btn-padding: $spacing-vertical * 2/3; - --btn-height: $spacing-vertical * 1.5; - --btn-intra-margin: $spacing-vertical; + --button-bg: var(--color-bg-alt); + --button-color: #fff; + --button-primary-bg: var(--color-primary); + --button-primary-color: #fff; + --button-padding: $spacing-vertical * 2/3; + --button-height: $spacing-vertical * 1.5; + --button-intra-margin: $spacing-vertical; + --button-radius: 3px; /* Header */ --header-bg: var(--color-bg); --header-color: #666; --header-active-color: rgba(0, 0, 0, 0.85); - --header-height: 65px; + --header-height: $spacing-vertical * 2.5; --header-button-bg: transparent; //var(--button-bg); --header-button-hover-bg: rgba(100, 100, 100, 0.15); @@ -144,6 +142,7 @@ $text-color: #000; --tooltip-width: 300px; --tooltip-bg: var(--color-bg); --tooltip-color: var(--text-color); + --tooltip-border: 1px solid #aaa; /* Scrollbar */ --scrollbar-radius: 10px; diff --git a/src/renderer/scss/component/_button.scss b/src/renderer/scss/component/_button.scss index dfb8e0513..576ca4c33 100644 --- a/src/renderer/scss/component/_button.scss +++ b/src/renderer/scss/component/_button.scss @@ -1,78 +1,89 @@ -/* -TODO: -Determine [disabled] or .disabled -Add support (probably just get rid of button prefix) -*/ +@import '../mixin/link.scss'; -button { - border: none; - text-decoration: none; - cursor: pointer; +$button-focus-shift: 12%; + +.button-set-item { position: relative; -} + display: inline-block; -button:disabled.btn--disabled { - cursor: default; - background-color: transparent; -} - -button.btn { - padding: 10px; - margin: 0 5px; - border-radius: var(--btn-radius); - color: var(--btn-primary-color); - background-color: var(--btn-primary-bg); - - &:hover:not(.btn--disabled) { - box-shadow: var(--box-shadow-layer); + + .button-set-item { + margin-left: var(--button-intra-margin); } } -button.btn.btn--alt { - color: var(--btn-alt-color); - background-color: #efefef; - - &:hover { - color: #111; +.button-block, +.faux-button-block { + display: inline-block; + height: var(--button-height); + line-height: var(--button-height); + text-decoration: none; + border: 0 none; + text-align: center; + border-radius: var(--button-radius); + text-transform: uppercase; + .icon { + top: 0em; } - - &:active { - background-color: #cdcdcd; + .icon:first-child { + padding-right: 5px; } + .icon:last-child { + padding-left: 5px; + } + .icon:only-child { + padding-left: 0; + padding-right: 0; + } +} +.button-block { + cursor: pointer; + font-weight: 500; + font-size: 14px; + user-select: none; + transition: background var(--animation-duration) var(--animation-style); +} - &:disabled { - color: var(--color-help); - background-color: transparent; +.button__content { + margin: 0 var(--button-padding); + display: flex; + .link-label { + text-decoration: none !important; } } -button.btn.btn--circle { - border-radius: 50%; - transition: all 0.2s; +.button-primary { + color: var(--button-primary-color); + background-color: var(--button-primary-bg); + box-shadow: var(--box-shadow-layer); - &:hover:not([disabled]) { - border-radius: var(--btn-radius); + &:focus { + //color: var(--button-primary-active-color); + //background-color:color: var(--button-primary-active-bg); + //box-shadow: $box-shadow-focus; } } - -button.btn.btn--inverse { - box-shadow: none; - background-color: transparent; - color: var(--btn-primary-bg); +.button-alt { + background-color: var(--button-bg); + box-shadow: var(--box-shadow-layer); } -button.btn--link { - padding: 0; - margin: 0; - background-color: inherit; - font-size: 0.9em; - color: var(--btn-primary-bg); // this should be a different color +.button-text { + @include text-link(); + display: inline-block; - &:hover { - border-bottom: 1px solid; + .button__content { + margin: 0 var(--text-link-padding); } } - -.btn__label { - padding: 0 5px; +.button-text-help { + @include text-link(var(--text-help-color)); + font-size: 0.8em; +} +.button--flat { + box-shadow: none !important; +} + +.button--submit { + font-family: inherit; + line-height: 0; } diff --git a/src/renderer/scss/component/_card.scss b/src/renderer/scss/component/_card.scss index 3315b8f31..cef50a1fb 100644 --- a/src/renderer/scss/component/_card.scss +++ b/src/renderer/scss/component/_card.scss @@ -2,19 +2,194 @@ margin-left: auto; margin-right: auto; max-width: var(--card-max-width); + background: var(--card-bg); + box-shadow: var(--box-shadow-layer); border-radius: var(--card-radius); + margin-bottom: var(--card-margin); overflow: auto; user-select: text; - display: flex; + + //below added to prevent scrollbar on long titles when show page loads, would prefer a cleaner CSS solution + overflow-x: hidden; +} +.card--obscured { + position: relative; +} +.card--obscured .card__inner { + filter: blur(var(--nsfw-blur-intensity)); +} +.card__title-primary, +.card__title-identity, +.card__content, +.card__subtext, +.card__actions { + padding: 0 var(--card-padding); } -.card--placeholder { - background-color: black; +.card--small { + .card__title-primary, + .card__title-identity, + .card__actions, + .card__content, + .card__subtext { + padding: 0 calc(var(--card-padding) / 2); + } +} +.card__title-primary { + margin-top: var(--card-margin); + margin-bottom: var(--card-margin); +} +.card__title-primary .meta { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.card__title-identity { + margin: 16px 0; +} +.card__actions { + margin-top: var(--card-margin); + margin-bottom: var(--card-margin); + user-select: none; +} +.card__actions--bottom { + margin-top: $spacing-vertical * 1/3; + margin-bottom: $spacing-vertical * 1/3; + border-top: var(--divider); +} +.card__actions--form-submit { + margin-top: $spacing-vertical; + margin-bottom: var(--card-margin); +} +.card__action--right { + float: right; +} +.card__content { + margin-top: var(--card-margin); + margin-bottom: var(--card-margin); + table:not(:last-child) { + margin-bottom: var(--card-margin); + } +} + +.card__actions--only-vertical { + margin-left: 0; + margin-right: 0; + padding-left: 0; + padding-right: 0; +} + +.card__content--extra-vertical-space { + margin: $spacing-vertical 0; +} + +$font-size-subtext-multiple: 0.82; +.card__subtext { + color: var(--color-meta-light); + font-size: calc(var(--font-size-subtext-multiple) * 1em); + margin-top: $spacing-vertical * 1/3; + margin-bottom: $spacing-vertical * 1/3; +} +.card__subtext--allow-newlines { + white-space: pre-wrap; +} +.card__subtext--two-lines { + height: calc( + var(--font-size) * var(--font-size-subtext-multiple) * var(--font-line-height) * 2 + ); /*this is so one line text still has the proper height*/ +} +.card-overlay { + position: absolute; + left: 0px; + right: 0px; + top: 0px; + bottom: 0px; + padding: 20px; + background-color: var(--color-dark-overlay); + color: #fff; + display: flex; + align-items: center; + font-weight: 600; +} + +.card__link { + display: block; + cursor: pointer; +} +.card--link { + transition: transform 0.2s var(--animation-style); +} +.card--link:hover { + position: relative; + z-index: 1; + box-shadow: var(--box-shadow-focus); + transform: scale(var(--card-link-scaling)) translateX(var(--card-hover-translate)); + transform-origin: 50% 50%; + overflow-x: visible; + overflow-y: visible; +} +.card--link:hover ~ .card--link { + transform: translateX(calc(var(--card-hover-translate) * 2)); +} + +.card__media { + background-size: cover; + background-repeat: no-repeat; + background-position: 50% 50%; +} + +.card__media--autothumb { + position: relative; +} +.card__media--autothumb.purple { + background-color: #9c27b0; +} +.card__media--autothumb.red { + background-color: #e53935; +} +.card__media--autothumb.pink { + background-color: #e91e63; +} +.card__media--autothumb.indigo { + background-color: #3f51b5; +} +.card__media--autothumb.blue { + background-color: #2196f3; +} +.card__media--autothumb.light-blue { + background-color: #039be5; +} +.card__media--autothumb.cyan { + background-color: #00acc1; +} +.card__media--autothumb.teal { + background-color: #009688; +} +.card__media--autothumb.green { + background-color: #43a047; +} +.card__media--autothumb.yellow { + background-color: #ffeb3b; +} +.card__media--autothumb.orange { + background-color: #ffa726; +} + +.card__media--autothumb .card__autothumb__text { + font-size: 2em; + width: 100%; + color: #ffffff; + text-align: center; + position: absolute; + top: 36%; +} + +.card__indicators { + float: right; } .card--small { width: var(--card-small-width); - min-height: var(--card-small-width); overflow-x: hidden; white-space: normal; } @@ -22,237 +197,126 @@ height: calc(var(--card-small-width) * 9 / 16); } -.card__link { - cursor: pointer; - - // TODO: hover animations - // :hover { - // - // } -} - -.card__media { - background-size: cover; - background-repeat: no-repeat; - background-position: 50% 50%; - background-color: var(--color-placeholder); -} - -.card__media--autothumb { - display: flex; - justify-content: center; - align-items: center; -} - -.card__title-identity { - margin-top: $spacing-vertical * 1/3; -} - -// TODO: regular .card__title for show page -.card__title--small { - font-weight: 600; - font-size: 0.9em; +.card--form { + width: calc(var(--input-width) + var(--card-padding) * 2); } .card__subtitle { color: var(--color-help); font-size: 0.85em; - padding-top: $spacing-vertical * 1/3; + line-height: calc(var(--font-line-height) * 1 / 0.85); } -// .card__title-primary .meta { -// white-space: nowrap; -// overflow: hidden; -// text-overflow: ellipsis; -// } -// +.card--file-subtitle { + display: flex; +} -// -// .card__actions { -// margin-top: var(--card-margin); -// margin-bottom: var(--card-margin); -// user-select: none; -// } -// -// .card__actions--bottom { -// margin-top: $spacing-vertical * 1/3; -// margin-bottom: $spacing-vertical * 1/3; -// border-top: var(--divider); -// } -// -// .card__actions--form-submit { -// margin-top: $spacing-vertical; -// margin-bottom: var(--card-margin); -// } -// -// .card__action--right { -// float: right; -// } -// -// .card__content { -// margin-top: var(--card-margin); -// margin-bottom: var(--card-margin); -// table:not(:last-child) { -// margin-bottom: var(--card-margin); -// } -// } -// -// .card__actions--only-vertical { -// margin-left: 0; -// margin-right: 0; -// padding-left: 0; -// padding-right: 0; -// } -// -// .card__content--extra-vertical-space { -// margin: $spacing-vertical 0; -// } -// -// $font-size-subtext-multiple: 0.82; -// .card__subtext { -// color: var(--color-meta-light); -// font-size: calc(var(--font-size-subtext-multiple) * 1em); -// margin-top: $spacing-vertical * 1/3; -// margin-bottom: $spacing-vertical * 1/3; -// } -// .card__subtext--allow-newlines { -// white-space: pre-wrap; -// } -// .card__subtext--two-lines { -// height: calc( -// var(--font-size) * var(--font-size-subtext-multiple) * var(--font-line-height) * 2 -// ); /*this is so one line text still has the proper height*/ -// } -// .card-overlay { -// position: absolute; -// left: 0px; -// right: 0px; -// top: 0px; -// bottom: 0px; -// padding: 20px; -// background-color: var(--color-dark-overlay); -// color: #fff; -// display: flex; -// align-items: center; -// font-weight: 600; -// } -// -// -// .card__media--autothumb { -// position: relative; -// } -// .card__media--autothumb.purple { -// background-color: #9c27b0; -// } -// .card__media--autothumb.red { -// background-color: #e53935; -// } -// .card__media--autothumb.pink { -// background-color: #e91e63; -// } -// .card__media--autothumb.indigo { -// background-color: #3f51b5; -// } -// .card__media--autothumb.blue { -// background-color: #2196f3; -// } -// .card__media--autothumb.light-blue { -// background-color: #039be5; -// } -// .card__media--autothumb.cyan { -// background-color: #00acc1; -// } -// .card__media--autothumb.teal { -// background-color: #009688; -// } -// .card__media--autothumb.green { -// background-color: #43a047; -// } -// .card__media--autothumb.yellow { -// background-color: #ffeb3b; -// } -// .card__media--autothumb.orange { -// background-color: #ffa726; -// } -// -// .card__media--autothumb .card__autothumb__text { -// font-size: 2em; -// width: 100%; -// color: #ffffff; -// text-align: center; -// position: absolute; -// top: 36%; -// } -// -// .card--form { -// width: calc(var(--input-width) + var(--card-padding) * 2); -// } -// +// this is too specific +// it should be a helper class +// ex. ".m-padding-left" +// will come back to this during the redesign - sean +.card__publish-date { + padding-left: 20px; +} -// -// .card-series-submit { -// margin-left: auto; -// margin-right: auto; -// max-width: var(--card-max-width); -// padding: $spacing-vertical / 2; -// } +.card-series-submit { + margin-left: auto; + margin-right: auto; + max-width: var(--card-max-width); + padding: $spacing-vertical / 2; +} -/* - .card-row is used on the discover page - It is a list of cards that extend past the right edge of the screen - There are left/right arrows to scroll the cards and view hidden content -*/ .card-row { + + .card-row { + margin-top: $spacing-vertical * 1/3; + } +} + +.card-row__placeholder { + padding-bottom: $spacing-vertical; +} + +$padding-top-card-hover-hack: 20px; +$padding-right-card-hover-hack: 30px; + +.card-row__items { + width: 100%; + overflow: hidden; + + /*hacky way to give space for hover */ + padding-top: $padding-top-card-hover-hack; + margin-top: -1 * $padding-top-card-hover-hack; + padding-right: $padding-right-card-hover-hack; + margin-right: -1 * $padding-right-card-hover-hack; + > .card { + vertical-align: top; + display: inline-block; + } + > .card + .card { + margin-left: 16px; + } +} + +.card-row--small { overflow: hidden; white-space: nowrap; width: 100%; min-width: var(--card-small-width); - padding-top: $spacing-vertical; + margin-right: $spacing-vertical; } - .card-row__header { - display: flex; - flex-direction: row; - justify-content: space-between; - // specific padding-left styling is needed here - // this needs to be used on a page with noPadding - // doing so allows the content to scroll to the edge of the screen - padding-left: $spacing-vertical * 2/3; -} - -.card-row__title { - display: flex; - align-items: center; + margin-bottom: 16px; } .card-row__scrollhouse { - padding-top: $spacing-vertical * 2/3; - overflow: hidden; + position: relative; + /*hacky way to give space for hover */ + padding-right: $padding-right-card-hover-hack; +} - .card { - display: inline-block; - vertical-align: top; - margin-left: $spacing-vertical * 2/3; - } +.card-row__nav { + position: absolute; + padding: 0 var(--card-margin); + height: 100%; + top: calc($padding-top-card-hover-hack - var(--card-margin)); +} +.card-row__nav .card-row__scroll-button { + background: var(--card-bg); + color: var(--color-help); + box-shadow: var(--box-shadow-layer); + padding: $spacing-vertical $spacing-vertical / 2; + position: absolute; + cursor: pointer; + left: 0; + top: 36%; + z-index: 2; + opacity: 0.8; + transition: transform 0.2s var(--animation-style); - .card:last-of-type { - padding-right: $spacing-vertical * 2/3; + &:hover { + opacity: 1; + transform: scale(calc(var(--card-link-scaling) * 1.1)); } } +.card-row__nav--left { + left: 0; +} +.card-row__nav--right { + right: 0; +} /* if we keep doing things like this, we should add a real grid system, but I'm going to be a selective dick about it - Jeremy */ -//TODO: css grid -// .card-grid { -// $margin-card-grid: $spacing-vertical * 2/3; -// display: flex; -// flex-wrap: wrap; -// > .card { -// width: $width-page-constrained / 2 - $margin-card-grid / 2; -// flex-grow: 1; -// } -// > .card:nth-of-type(2n - 1):not(:last-child) { -// margin-right: $margin-card-grid; -// } -// } +.card-grid { + $margin-card-grid: $spacing-vertical * 2/3; + display: flex; + flex-wrap: wrap; + > .card { + width: $width-page-constrained / 2 - $margin-card-grid / 2; + flex-grow: 1; + } + > .card:nth-of-type(2n - 1):not(:last-child) { + margin-right: $margin-card-grid; + } +} diff --git a/src/renderer/scss/component/_channel-indicator.scss b/src/renderer/scss/component/_channel-indicator.scss index 2291cc933..7c437780c 100644 --- a/src/renderer/scss/component/_channel-indicator.scss +++ b/src/renderer/scss/component/_channel-indicator.scss @@ -5,6 +5,12 @@ text-overflow: ellipsis; } +// this shouldn't know about the card width +// will come back to this for the redesign - sean +.channel-name--small { + width: calc(var(--card-small-width) * 2 / 3); +} + .channel-indicator__icon--invalid { color: var(--color-error); } diff --git a/src/renderer/scss/component/_header.scss b/src/renderer/scss/component/_header.scss index ca07bcc13..a8967d0eb 100644 --- a/src/renderer/scss/component/_header.scss +++ b/src/renderer/scss/component/_header.scss @@ -1,60 +1,64 @@ #header { + color: var(--header-color); + background: var(--header-bg); display: flex; align-items: center; + justify-content: space-around; position: fixed; + box-shadow: var(--box-shadow-layer); top: 0; left: 0; width: 100%; - height: var(--header-height); z-index: 3; + padding: $spacing-vertical / 2; box-sizing: border-box; - color: var(--header-color); - background-color: var(--header-bg); } - -.header__actions-left { - display: flex; - padding: 0 5px; -} - -.header__actions-right { - margin-left: auto; -} - -.header__wunderbar { - flex: 1; - max-width: 325px; - min-width: 175px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - height: 100%; - display: flex; - align-items: center; - padding: 10px 5px; - cursor: text; -} - -.wunderbar__input { - height: 50%; - width: 100%; - color: var(--search-color); - padding: 10px; - background-color: #f3f3f3; - border-radius: 10px; - font-size: 0.9em; - - &:focus { - // TODO: focus style +.header__item { + padding-left: $spacing-vertical / 4; + padding-right: $spacing-vertical / 4; + .button-alt { + background: var(--header-button-bg) !important; + font-size: 1em; + } + .button-alt:hover { + background: var(--header-button-hover-bg) !important; } } -.wunderbar__suggestion { - padding: 5px; - background-color: var(--header-bg); - cursor: pointer; +.header__item--wunderbar { + flex-grow: 1; } -.wunderbar__active-suggestion { - background-color: #a3ffb0; +.wunderbar { + position: relative; + .icon { + position: absolute; + left: 10px; + top: $spacing-vertical / 2 - 4px; //hacked + } +} + +.wunderbar--active .icon-search { + color: var(--color-primary); +} + +// below styles should be inside the common input styling +// will come back to this with the redesign - sean +.wunderbar__input { + background: var(--search-bg); + width: 100%; + color: var(--search-color); + height: $spacing-vertical * 1.5; + line-height: $spacing-vertical * 1.5; + padding-left: 38px; + padding-right: 5px; + border-radius: 2px; + border: var(--search-border); + transition: box-shadow var(--transition-duration) var(--transition-type); + &:focus { + background: var(--search-active-bg); + color: var(--search-active-color); + box-shadow: var(--search-active-shadow); + border-color: var(--color-primary); + } } diff --git a/src/renderer/scss/component/_tooltip.scss b/src/renderer/scss/component/_tooltip.scss index 1448fd590..d017996c1 100644 --- a/src/renderer/scss/component/_tooltip.scss +++ b/src/renderer/scss/component/_tooltip.scss @@ -2,7 +2,10 @@ .tooltip { position: relative; - padding: 0 $spacing-vertical / 3; +} + +.tooltip__link { + @include text-link(); } .tooltip__body { @@ -14,15 +17,16 @@ box-sizing: border-box; padding: $spacing-vertical / 2; width: var(--tooltip-width); + border: var(--tooltip-border); color: var(--tooltip-color); background-color: var(--tooltip-bg); font-size: calc(var(--font-size) * 7/8); line-height: var(--font-line-height); box-shadow: var(--box-shadow-layer); - border-radius: var(--card-radius); } -.tooltip__link { +.tooltip--header .tooltip__link { + @include text-link(#aaa); font-size: calc(var(--font-size) * 3/4); margin-left: var(--button-padding); vertical-align: middle; diff --git a/static/font/metropolis/Metropolis-BlackItalic.woff2 b/static/font/metropolis/Metropolis-BlackItalic.woff2 deleted file mode 100755 index 942bf6452..000000000 Binary files a/static/font/metropolis/Metropolis-BlackItalic.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-Bold.woff2 b/static/font/metropolis/Metropolis-Bold.woff2 deleted file mode 100755 index 54bb59f65..000000000 Binary files a/static/font/metropolis/Metropolis-Bold.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-BoldItalic.woff2 b/static/font/metropolis/Metropolis-BoldItalic.woff2 deleted file mode 100755 index dbb3f0425..000000000 Binary files a/static/font/metropolis/Metropolis-BoldItalic.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-ExtraBold.woff2 b/static/font/metropolis/Metropolis-ExtraBold.woff2 deleted file mode 100755 index d3ce18b1d..000000000 Binary files a/static/font/metropolis/Metropolis-ExtraBold.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-ExtraBoldItalic.woff2 b/static/font/metropolis/Metropolis-ExtraBoldItalic.woff2 deleted file mode 100755 index 21d8ab53e..000000000 Binary files a/static/font/metropolis/Metropolis-ExtraBoldItalic.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-ExtraLight.woff2 b/static/font/metropolis/Metropolis-ExtraLight.woff2 deleted file mode 100755 index 993d8c421..000000000 Binary files a/static/font/metropolis/Metropolis-ExtraLight.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-ExtraLightItalic.woff2 b/static/font/metropolis/Metropolis-ExtraLightItalic.woff2 deleted file mode 100755 index deffa671b..000000000 Binary files a/static/font/metropolis/Metropolis-ExtraLightItalic.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-Light.woff2 b/static/font/metropolis/Metropolis-Light.woff2 deleted file mode 100755 index 3b20f0723..000000000 Binary files a/static/font/metropolis/Metropolis-Light.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-LightItalic.woff2 b/static/font/metropolis/Metropolis-LightItalic.woff2 deleted file mode 100755 index f3c0e4095..000000000 Binary files a/static/font/metropolis/Metropolis-LightItalic.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-Medium.woff2 b/static/font/metropolis/Metropolis-Medium.woff2 deleted file mode 100755 index d5aabb6e9..000000000 Binary files a/static/font/metropolis/Metropolis-Medium.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-MediumItalic.woff2 b/static/font/metropolis/Metropolis-MediumItalic.woff2 deleted file mode 100755 index 506fb07b1..000000000 Binary files a/static/font/metropolis/Metropolis-MediumItalic.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-Regular.woff2 b/static/font/metropolis/Metropolis-Regular.woff2 deleted file mode 100755 index 40417bff0..000000000 Binary files a/static/font/metropolis/Metropolis-Regular.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-RegularItalic.woff2 b/static/font/metropolis/Metropolis-RegularItalic.woff2 deleted file mode 100755 index 956e0adb8..000000000 Binary files a/static/font/metropolis/Metropolis-RegularItalic.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-SemiBold.woff2 b/static/font/metropolis/Metropolis-SemiBold.woff2 deleted file mode 100755 index 2ad1a7e00..000000000 Binary files a/static/font/metropolis/Metropolis-SemiBold.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-SemiBoldItalic.woff2 b/static/font/metropolis/Metropolis-SemiBoldItalic.woff2 deleted file mode 100755 index 94df7d35e..000000000 Binary files a/static/font/metropolis/Metropolis-SemiBoldItalic.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-Thin.woff2 b/static/font/metropolis/Metropolis-Thin.woff2 deleted file mode 100755 index 0c370707f..000000000 Binary files a/static/font/metropolis/Metropolis-Thin.woff2 and /dev/null differ diff --git a/static/font/metropolis/Metropolis-ThinItalic.woff2 b/static/font/metropolis/Metropolis-ThinItalic.woff2 deleted file mode 100755 index 85491c2e2..000000000 Binary files a/static/font/metropolis/Metropolis-ThinItalic.woff2 and /dev/null differ diff --git a/yarn.lock b/yarn.lock index 02f395c04..35e76177e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2661,10 +2661,6 @@ dom-converter@~0.1: dependencies: utila "~0.3" -dom-scroll-into-view@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/dom-scroll-into-view/-/dom-scroll-into-view-1.2.1.tgz#e8f36732dd089b0201a88d7815dc3f88e6d66c7e" - dom-serializer@0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"