mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Project Aphrodite
This commit is contained in:
16
frontend/src/Components/Page/Toolbar/PageToolbar.css
Normal file
16
frontend/src/Components/Page/Toolbar/PageToolbar.css
Normal file
@@ -0,0 +1,16 @@
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 auto;
|
||||
padding: 0 20px;
|
||||
height: $toolbarHeight;
|
||||
background-color: $toolbarBackgroundColor;
|
||||
color: $toolbarColor;
|
||||
line-height: 60px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.toolbar {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
33
frontend/src/Components/Page/Toolbar/PageToolbar.js
Normal file
33
frontend/src/Components/Page/Toolbar/PageToolbar.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import styles from './PageToolbar.css';
|
||||
|
||||
class PageToolbar extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
children
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PageToolbar.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
PageToolbar.defaultProps = {
|
||||
className: styles.toolbar
|
||||
};
|
||||
|
||||
export default PageToolbar;
|
32
frontend/src/Components/Page/Toolbar/PageToolbarButton.css
Normal file
32
frontend/src/Components/Page/Toolbar/PageToolbarButton.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.toolbarButton {
|
||||
composes: link from 'Components/Link/Link.css';
|
||||
|
||||
width: $toolbarButtonWidth;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
color: $toobarButtonHoverColor;
|
||||
}
|
||||
|
||||
&.isDisabled {
|
||||
color: $disabledColor;
|
||||
}
|
||||
}
|
||||
|
||||
.isDisabled {
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.labelContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0 3px;
|
||||
color: $toolbarLabelColor;
|
||||
font-size: $extraSmallFontSize;
|
||||
line-height: calc($extraSmallFontSize + 1px);
|
||||
}
|
57
frontend/src/Components/Page/Toolbar/PageToolbarButton.js
Normal file
57
frontend/src/Components/Page/Toolbar/PageToolbarButton.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import styles from './PageToolbarButton.css';
|
||||
|
||||
function PageToolbarButton(props) {
|
||||
const {
|
||||
label,
|
||||
iconName,
|
||||
spinningName,
|
||||
isDisabled,
|
||||
isSpinning,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.toolbarButton,
|
||||
isDisabled && styles.isDisabled
|
||||
)}
|
||||
isDisabled={isDisabled || isSpinning}
|
||||
{...otherProps}
|
||||
>
|
||||
<Icon
|
||||
name={isSpinning ? (spinningName || iconName) : iconName}
|
||||
isSpinning={isSpinning}
|
||||
size={21}
|
||||
/>
|
||||
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
PageToolbarButton.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
iconName: PropTypes.object.isRequired,
|
||||
spinningName: PropTypes.object,
|
||||
isSpinning: PropTypes.bool,
|
||||
isDisabled: PropTypes.bool
|
||||
};
|
||||
|
||||
PageToolbarButton.defaultProps = {
|
||||
spinningName: icons.SPINNER,
|
||||
isDisabled: false,
|
||||
isSpinning: false
|
||||
};
|
||||
|
||||
export default PageToolbarButton;
|
40
frontend/src/Components/Page/Toolbar/PageToolbarSection.css
Normal file
40
frontend/src/Components/Page/Toolbar/PageToolbarSection.css
Normal file
@@ -0,0 +1,40 @@
|
||||
.sectionContainer {
|
||||
display: flex;
|
||||
flex: 1 1 10%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.overflowMenuButton {
|
||||
composes: menuButton from 'Components/Menu/ToolbarMenuButton.css';
|
||||
}
|
||||
|
||||
.overflowMenuItemIcon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.overflowMenuButton {
|
||||
&::after {
|
||||
margin-left: 0;
|
||||
content: '\25BE';
|
||||
}
|
||||
}
|
||||
}
|
221
frontend/src/Components/Page/Toolbar/PageToolbarSection.js
Normal file
221
frontend/src/Components/Page/Toolbar/PageToolbarSection.js
Normal file
@@ -0,0 +1,221 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { forEach } from 'Helpers/elementChildren';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import SpinnerIcon from 'Components/SpinnerIcon';
|
||||
import Measure from 'Components/Measure';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton';
|
||||
import styles from './PageToolbarSection.css';
|
||||
|
||||
const BUTTON_WIDTH = parseInt(dimensions.toolbarButtonWidth);
|
||||
const SEPARATOR_MARGIN = parseInt(dimensions.toolbarSeparatorMargin);
|
||||
const SEPARATOR_WIDTH = 2 * SEPARATOR_MARGIN + 1;
|
||||
const SEPARATOR_NAME = 'PageToolbarSeparator';
|
||||
|
||||
function calculateOverflowItems(children, isMeasured, width, collapseButtons) {
|
||||
let buttonCount = 0;
|
||||
let separatorCount = 0;
|
||||
const validChildren = [];
|
||||
|
||||
forEach(children, (child) => {
|
||||
const name = child.type.name;
|
||||
|
||||
if (name === SEPARATOR_NAME) {
|
||||
separatorCount++;
|
||||
} else {
|
||||
buttonCount++;
|
||||
}
|
||||
|
||||
validChildren.push(child);
|
||||
});
|
||||
|
||||
const buttonsWidth = buttonCount * BUTTON_WIDTH;
|
||||
const separatorsWidth = separatorCount + SEPARATOR_WIDTH;
|
||||
const totalWidth = buttonsWidth + separatorsWidth;
|
||||
|
||||
// If the width of buttons and separators is less than
|
||||
// the available width return all valid children.
|
||||
|
||||
if (
|
||||
!isMeasured ||
|
||||
!collapseButtons ||
|
||||
totalWidth < width
|
||||
) {
|
||||
return {
|
||||
buttons: validChildren,
|
||||
buttonCount,
|
||||
overflowItems: []
|
||||
};
|
||||
}
|
||||
|
||||
const maxButtons = Math.max(Math.floor((width - separatorsWidth) / BUTTON_WIDTH), 1);
|
||||
const buttons = [];
|
||||
const overflowItems = [];
|
||||
let actualButtons = 0;
|
||||
|
||||
// Return all buttons if only one is being pushed to the overflow menu.
|
||||
if (buttonCount - 1 === maxButtons) {
|
||||
return {
|
||||
buttons: validChildren,
|
||||
buttonCount,
|
||||
overflowItems: []
|
||||
};
|
||||
}
|
||||
|
||||
validChildren.forEach((child, index) => {
|
||||
if (actualButtons < maxButtons) {
|
||||
if (child.type.name !== SEPARATOR_NAME) {
|
||||
buttons.push(child);
|
||||
actualButtons++;
|
||||
}
|
||||
} else if (child.type.name !== SEPARATOR_NAME) {
|
||||
overflowItems.push(child.props);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
buttons,
|
||||
buttonCount,
|
||||
overflowItems
|
||||
};
|
||||
}
|
||||
|
||||
class PageToolbarSection extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isMeasured: false,
|
||||
width: 0,
|
||||
buttons: [],
|
||||
overflowItems: []
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMeasure = ({ width }) => {
|
||||
this.setState({
|
||||
isMeasured: true,
|
||||
width
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
alignContent,
|
||||
collapseButtons
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isMeasured,
|
||||
width
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
buttons,
|
||||
buttonCount,
|
||||
overflowItems
|
||||
} = calculateOverflowItems(children, isMeasured, width, collapseButtons);
|
||||
|
||||
return (
|
||||
<Measure
|
||||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<div
|
||||
className={styles.sectionContainer}
|
||||
style={{
|
||||
flexGrow: buttonCount
|
||||
}}
|
||||
>
|
||||
{
|
||||
isMeasured ?
|
||||
<div className={classNames(
|
||||
styles.section,
|
||||
styles[alignContent]
|
||||
)}
|
||||
>
|
||||
{
|
||||
buttons.map((button) => {
|
||||
return button;
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
!!overflowItems.length &&
|
||||
<Menu>
|
||||
<ToolbarMenuButton
|
||||
className={styles.overflowMenuButton}
|
||||
iconName={icons.OVERFLOW}
|
||||
text="More"
|
||||
/>
|
||||
|
||||
<MenuContent>
|
||||
{
|
||||
overflowItems.map((item) => {
|
||||
const {
|
||||
iconName,
|
||||
spinningName,
|
||||
label,
|
||||
isDisabled,
|
||||
isSpinning,
|
||||
...otherProps
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={label}
|
||||
isDisabled={isDisabled || isSpinning}
|
||||
{...otherProps}
|
||||
>
|
||||
<SpinnerIcon
|
||||
className={styles.overflowMenuItemIcon}
|
||||
name={iconName}
|
||||
spinningName={spinningName}
|
||||
isSpinning={isSpinning}
|
||||
/>
|
||||
{label}
|
||||
</MenuItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PageToolbarSection.propTypes = {
|
||||
children: PropTypes.node,
|
||||
alignContent: PropTypes.oneOf([align.LEFT, align.CENTER, align.RIGHT]),
|
||||
collapseButtons: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
PageToolbarSection.defaultProps = {
|
||||
alignContent: align.LEFT,
|
||||
collapseButtons: true
|
||||
};
|
||||
|
||||
export default PageToolbarSection;
|
@@ -0,0 +1,12 @@
|
||||
.separator {
|
||||
margin: 10px $toolbarSeparatorMargin;
|
||||
height: 40px;
|
||||
border-right: 1px solid #e5e5e5;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.separator {
|
||||
margin: 10px 5px;
|
||||
}
|
||||
}
|
17
frontend/src/Components/Page/Toolbar/PageToolbarSeparator.js
Normal file
17
frontend/src/Components/Page/Toolbar/PageToolbarSeparator.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React, { Component } from 'react';
|
||||
import styles from './PageToolbarSeparator.css';
|
||||
|
||||
class PageToolbarSeparator extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.separator} />
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default PageToolbarSeparator;
|
Reference in New Issue
Block a user