mirror of
https://github.com/Readarr/Readarr
synced 2025-12-10 02:15:16 +01:00
New: Scroll long book titles
This commit is contained in:
parent
72de94308b
commit
106a1c339b
2 changed files with 237 additions and 43 deletions
|
|
@ -14,6 +14,8 @@ import Icon from 'Components/Icon';
|
|||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Marquee from 'Components/Marquee';
|
||||
import Measure from 'Components/Measure';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
|
|
@ -57,7 +59,8 @@ class BookDetails extends Component {
|
|||
isRetagModalOpen: false,
|
||||
isEditBookModalOpen: false,
|
||||
isDeleteBookModalOpen: false,
|
||||
selectedTabIndex: 0
|
||||
selectedTabIndex: 0,
|
||||
titleWidth: 0
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +106,10 @@ class BookDetails extends Component {
|
|||
this.setState({ selectedTabIndex: index });
|
||||
}
|
||||
|
||||
onTitleMeasure = ({ width }) => {
|
||||
this.setState({ titleWidth: width });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
|
@ -141,9 +148,12 @@ class BookDetails extends Component {
|
|||
isRetagModalOpen,
|
||||
isEditBookModalOpen,
|
||||
isDeleteBookModalOpen,
|
||||
selectedTabIndex
|
||||
selectedTabIndex,
|
||||
titleWidth
|
||||
} = this.state;
|
||||
|
||||
const marqueeWidth = (titleWidth - 150);
|
||||
|
||||
return (
|
||||
<PageContent title={title}>
|
||||
<PageToolbar>
|
||||
|
|
@ -217,51 +227,53 @@ class BookDetails extends Component {
|
|||
/>
|
||||
|
||||
<div className={styles.info}>
|
||||
<div className={styles.titleRow}>
|
||||
<div className={styles.titleContainer}>
|
||||
<Measure onMeasure={this.onTitleMeasure}>
|
||||
<div className={styles.titleRow}>
|
||||
<div className={styles.titleContainer}>
|
||||
|
||||
<div className={styles.toggleMonitoredContainer}>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
isSaving={isSaving}
|
||||
size={40}
|
||||
onPress={onMonitorTogglePress}
|
||||
<div className={styles.toggleMonitoredContainer}>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
isSaving={isSaving}
|
||||
size={40}
|
||||
onPress={onMonitorTogglePress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.title} style={{ width: marqueeWidth }}>
|
||||
<Marquee text={title} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.bookNavigationButtons}>
|
||||
<IconButton
|
||||
className={styles.bookNavigationButton}
|
||||
name={icons.ARROW_LEFT}
|
||||
size={30}
|
||||
title={`Go to ${previousBook.title}`}
|
||||
to={`/book/${previousBook.titleSlug}`}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.bookNavigationButton}
|
||||
name={icons.ARROW_UP}
|
||||
size={30}
|
||||
title={`Go to ${author.authorName}`}
|
||||
to={`/author/${author.titleSlug}`}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.bookNavigationButton}
|
||||
name={icons.ARROW_RIGHT}
|
||||
size={30}
|
||||
title={`Go to ${nextBook.title}`}
|
||||
to={`/book/${nextBook.titleSlug}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.bookNavigationButtons}>
|
||||
<IconButton
|
||||
className={styles.bookNavigationButton}
|
||||
name={icons.ARROW_LEFT}
|
||||
size={30}
|
||||
title={`Go to ${previousBook.title}`}
|
||||
to={`/book/${previousBook.titleSlug}`}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.bookNavigationButton}
|
||||
name={icons.ARROW_UP}
|
||||
size={30}
|
||||
title={`Go to ${author.authorName}`}
|
||||
to={`/author/${author.titleSlug}`}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.bookNavigationButton}
|
||||
name={icons.ARROW_RIGHT}
|
||||
size={30}
|
||||
title={`Go to ${nextBook.title}`}
|
||||
to={`/book/${nextBook.titleSlug}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Measure>
|
||||
|
||||
<div className={styles.details}>
|
||||
<div>
|
||||
|
|
@ -497,6 +509,7 @@ BookDetails.propTypes = {
|
|||
author: PropTypes.object,
|
||||
previousBook: PropTypes.object,
|
||||
nextBook: PropTypes.object,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onMonitorTogglePress: PropTypes.func.isRequired,
|
||||
onRefreshPress: PropTypes.func,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
|
|
|
|||
181
frontend/src/Components/Marquee.js
Normal file
181
frontend/src/Components/Marquee.js
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
const FPS = 20;
|
||||
const STEP = 1;
|
||||
const TIMEOUT = 1 / FPS * 1000;
|
||||
|
||||
class Marquee extends Component {
|
||||
|
||||
static propTypes = {
|
||||
text: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
hoverToStop: PropTypes.bool,
|
||||
loop: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
text: '',
|
||||
title: '',
|
||||
hoverToStop: true,
|
||||
loop: false
|
||||
};
|
||||
|
||||
state = {
|
||||
animatedWidth: 0,
|
||||
overflowWidth: 0,
|
||||
direction: 0
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.measureText();
|
||||
|
||||
if (this.props.hoverToStop) {
|
||||
this.startAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.text.length !== nextProps.text.length) {
|
||||
clearTimeout(this.marqueeTimer);
|
||||
this.setState({ animatedWidth: 0, direction: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.measureText();
|
||||
|
||||
if (this.props.hoverToStop) {
|
||||
this.startAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.marqueeTimer);
|
||||
}
|
||||
|
||||
onHandleMouseEnter = () => {
|
||||
if (this.props.hoverToStop) {
|
||||
clearTimeout(this.marqueeTimer);
|
||||
} else if (this.state.overflowWidth > 0) {
|
||||
this.startAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
onHandleMouseLeave = () => {
|
||||
if (this.props.hoverToStop && this.state.overflowWidth > 0) {
|
||||
this.startAnimation();
|
||||
} else {
|
||||
clearTimeout(this.marqueeTimer);
|
||||
this.setState({ animatedWidth: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
startAnimation = () => {
|
||||
clearTimeout(this.marqueeTimer);
|
||||
const isLeading = this.state.animatedWidth === 0;
|
||||
const timeout = isLeading ? 0 : TIMEOUT;
|
||||
|
||||
const animate = () => {
|
||||
const { overflowWidth } = this.state;
|
||||
let animatedWidth = this.state.animatedWidth;
|
||||
let direction = this.state.direction;
|
||||
|
||||
if (direction === 0) {
|
||||
animatedWidth = this.state.animatedWidth + STEP;
|
||||
} else {
|
||||
animatedWidth = this.state.animatedWidth - STEP;
|
||||
}
|
||||
|
||||
const isRoundOver = animatedWidth < 0;
|
||||
const endOfText = animatedWidth > overflowWidth;
|
||||
|
||||
if (endOfText) {
|
||||
direction = direction === 1;
|
||||
}
|
||||
|
||||
if (isRoundOver) {
|
||||
if (this.props.loop) {
|
||||
direction = direction === 0;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ animatedWidth, direction });
|
||||
this.marqueeTimer = setTimeout(animate, TIMEOUT);
|
||||
};
|
||||
|
||||
this.marqueeTimer = setTimeout(animate, timeout);
|
||||
}
|
||||
|
||||
measureText = () => {
|
||||
const container = this.container;
|
||||
const node = this.text;
|
||||
|
||||
if (container && node) {
|
||||
const containerWidth = container.offsetWidth;
|
||||
const textWidth = node.offsetWidth;
|
||||
const overflowWidth = textWidth - containerWidth;
|
||||
|
||||
if (overflowWidth !== this.state.overflowWidth) {
|
||||
this.setState({ overflowWidth });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = {
|
||||
position: 'relative',
|
||||
right: this.state.animatedWidth,
|
||||
whiteSpace: 'nowrap'
|
||||
};
|
||||
|
||||
if (this.state.overflowWidth < 0) {
|
||||
return (
|
||||
<div
|
||||
ref={(el) => {
|
||||
this.container = el;
|
||||
}}
|
||||
className={`ui-marquee ${this.props.className}`}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
<span
|
||||
ref={(el) => {
|
||||
this.text = el;
|
||||
}}
|
||||
style={style}
|
||||
title={(this.props.title && (this.props.text !== this.props.title)) ? `Original Title: ${this.props.title}` : this.props.text}
|
||||
>
|
||||
{this.props.text}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(el) => {
|
||||
this.container = el;
|
||||
}}
|
||||
className={`ui-marquee ${this.props.className}`.trim()}
|
||||
style={{ overflow: 'hidden' }}
|
||||
onMouseEnter={this.onHandleMouseEnter}
|
||||
onMouseLeave={this.onHandleMouseLeave}
|
||||
>
|
||||
<span
|
||||
ref={(el) => {
|
||||
this.text = el;
|
||||
}}
|
||||
style={style}
|
||||
title={(this.props.title && (this.props.text !== this.props.title)) ? `Original Title: ${this.props.title}` : this.props.text}
|
||||
>
|
||||
{this.props.text}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Marquee;
|
||||
Loading…
Reference in a new issue