Initial Push

This commit is contained in:
Qstick
2020-10-18 04:18:35 -04:00
parent 09ed2ab889
commit e1020f4107
1845 changed files with 1516 additions and 113975 deletions

View File

@@ -176,7 +176,7 @@ function HistoryDetails(props) {
reasonMessage = 'File was deleted by via UI';
break;
case 'MissingFromDisk':
reasonMessage = 'Radarr was unable to find the file on disk so it was removed';
reasonMessage = 'Prowlarr was unable to find the file on disk so it was removed';
break;
case 'Upgrade':
reasonMessage = 'File was deleted to import an upgrade';

View File

@@ -159,7 +159,7 @@ class AddNewMovie extends Component {
{translate('YouCanAlsoSearch')}
</div>
<div>
<Link to="https://github.com/Radarr/Radarr/wiki/FAQ#why-cant-i-add-a-new-movie-when-i-know-the-tmdb-id">
<Link to="https://github.com/Prowlarr/Prowlarr/wiki/FAQ#why-cant-i-add-a-new-movie-when-i-know-the-tmdb-id">
{translate('CantFindMovie')}
</Link>
</div>

View File

@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions';
import parseUrl from 'Utilities/String/parseUrl';
import AddNewMovie from './AddNewMovie';
@@ -28,8 +27,7 @@ function createMapStateToProps() {
const mapDispatchToProps = {
lookupMovie,
clearAddMovie,
fetchRootFolders,
fetchImportExclusions
fetchRootFolders
};
class AddNewMovieConnector extends Component {
@@ -45,7 +43,6 @@ class AddNewMovieConnector extends Component {
componentDidMount() {
this.props.fetchRootFolders();
this.props.fetchImportExclusions();
}
componentWillUnmount() {
@@ -101,8 +98,7 @@ AddNewMovieConnector.propTypes = {
term: PropTypes.string,
lookupMovie: PropTypes.func.isRequired,
clearAddMovie: PropTypes.func.isRequired,
fetchRootFolders: PropTypes.func.isRequired,
fetchImportExclusions: PropTypes.func.isRequired
fetchRootFolders: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieConnector);

View File

@@ -6,7 +6,6 @@ import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import MoviePoster from 'Movie/MoviePoster';
import translate from 'Utilities/String/translate';
import AddNewMovieModal from './AddNewMovieModal';
@@ -52,8 +51,6 @@ class AddNewMovieSearchResult extends Component {
render() {
const {
tmdbId,
imdbId,
youTubeTrailerId,
title,
titleSlug,
year,
@@ -163,13 +160,6 @@ class AddNewMovieSearchResult extends Component {
</span>
</Label>
}
tooltip={
<MovieDetailsLinks
tmdbId={tmdbId}
youTubeTrailerId={youTubeTrailerId}
imdbId={imdbId}
/>
}
canFlip={true}
kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM}

View File

@@ -48,7 +48,7 @@ class ImportMovieSelectFolderConnector extends Component {
const newRootFolders = _.differenceBy(items, prevProps.items, (item) => item.id);
if (newRootFolders.length === 1) {
this.props.push(`${window.Radarr.urlBase}/add/import/${newRootFolders[0].id}`);
this.props.push(`${window.Prowlarr.urlBase}/add/import/${newRootFolders[0].id}`);
}
}
}

View File

@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
function App({ store, history }) {
return (
<DocumentTitle title="Radarr">
<DocumentTitle title="Prowlarr">
<Provider store={store}>
<ConnectedRouter history={history}>
<PageConnector>

View File

@@ -6,22 +6,12 @@ import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector';
import MovieIndexConnector from 'Movie/Index/MovieIndexConnector';
import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import Profiles from 'Settings/Profiles/Profiles';
import Quality from 'Settings/Quality/Quality';
import Settings from 'Settings/Settings';
import TagSettings from 'Settings/Tags/TagSettings';
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
@@ -51,7 +41,7 @@ function AppRoutes(props) {
/>
{
window.Radarr.urlBase &&
window.Prowlarr.urlBase &&
<Route
exact={true}
path="/"
@@ -77,25 +67,6 @@ function AppRoutes(props) {
component={ImportMovies}
/>
<Route
path="/add/discover"
component={DiscoverMovieConnector}
/>
<Route
path="/movie/:titleSlug"
component={MovieDetailsPageConnector}
/>
{/*
Calendar
*/}
<Route
path="/calendar"
component={CalendarPageConnector}
/>
{/*
Activity
*/}
@@ -125,51 +96,16 @@ function AppRoutes(props) {
component={Settings}
/>
<Route
path="/settings/mediamanagement"
component={MediaManagementConnector}
/>
<Route
path="/settings/profiles"
component={Profiles}
/>
<Route
path="/settings/quality"
component={Quality}
/>
<Route
path="/settings/customformats"
component={CustomFormatSettingsConnector}
/>
<Route
path="/settings/indexers"
component={IndexerSettingsConnector}
/>
<Route
path="/settings/downloadclients"
component={DownloadClientSettingsConnector}
/>
<Route
path="/settings/importlists"
component={ImportListSettingsConnector}
/>
<Route
path="/settings/connect"
component={NotificationSettings}
/>
<Route
path="/settings/metadata"
component={MetadataSettings}
/>
<Route
path="/settings/tags"
component={TagSettings}

View File

@@ -26,12 +26,12 @@ function AppUpdatedModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Radarr Updated
Prowlarr Updated
</ModalHeader>
<ModalBody>
<div>
Version <span className={styles.version}>{version}</span> of Radarr has been installed, in order to get the latest changes you'll need to reload Radarr.
Version <span className={styles.version}>{version}</span> of Prowlarr has been installed, in order to get the latest changes you'll need to reload Prowlarr.
</div>
{

View File

@@ -33,7 +33,7 @@ function createMapDispatchToProps(dispatch, props) {
},
onSeeChangesPress() {
window.location = `${window.Radarr.urlBase}/system/updates`;
window.location = `${window.Prowlarr.urlBase}/system/updates`;
}
};
}

View File

@@ -1,3 +0,0 @@
.agenda {
margin-top: 10px;
}

View File

@@ -1,69 +0,0 @@
import moment from 'moment';
import PropTypes from 'prop-types';
import React from 'react';
import AgendaEventConnector from './AgendaEventConnector';
import styles from './Agenda.css';
function Agenda(props) {
const {
items,
start,
end
} = props;
const startDateParsed = Date.parse(start);
const endDateParsed = Date.parse(end);
items.forEach((item) => {
const cinemaDateParsed = Date.parse(item.inCinemas);
const digitalDateParsed = Date.parse(item.digitalRelease);
const physicalDateParsed = Date.parse(item.physicalRelease);
const dates = [];
if (cinemaDateParsed > 0 && cinemaDateParsed >= startDateParsed && cinemaDateParsed <= endDateParsed) {
dates.push(cinemaDateParsed);
}
if (digitalDateParsed > 0 && digitalDateParsed >= startDateParsed && digitalDateParsed <= endDateParsed) {
dates.push(digitalDateParsed);
}
if (physicalDateParsed > 0 && physicalDateParsed >= startDateParsed && physicalDateParsed <= endDateParsed) {
dates.push(physicalDateParsed);
}
item.sortDate = Math.min(...dates);
item.cinemaDateParsed = cinemaDateParsed;
item.digitalDateParsed = digitalDateParsed;
item.physicalDateParsed = physicalDateParsed;
});
items.sort((a, b) => ((a.sortDate > b.sortDate) ? 1 : -1));
return (
<div className={styles.agenda}>
{
items.map((item, index) => {
const momentDate = moment(item.inCinemas);
const showDate = index === 0 ||
!moment(items[index - 1].inCinemas).isSame(momentDate, 'day');
return (
<AgendaEventConnector
key={item.id}
movieId={item.id}
showDate={showDate}
{...item}
/>
);
})
}
</div>
);
}
Agenda.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
start: PropTypes.string.isRequired,
end: PropTypes.string.isRequired
};
export default Agenda;

View File

@@ -1,14 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import Agenda from './Agenda';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar,
(calendar) => {
return calendar;
}
);
}
export default connect(createMapStateToProps)(Agenda);

View File

@@ -1,96 +0,0 @@
.event {
display: flex;
overflow-x: hidden;
padding: 5px;
border-bottom: 1px solid $borderColor;
font-size: $defaultFontSize;
&:hover {
background-color: $tableRowHoverBackgroundColor;
}
}
.link {
composes: link from '~Calendar/Events/CalendarEvent.css';
}
.eventWrapper {
display: flex;
flex: 1 0 1px;
overflow-x: hidden;
padding-left: 6px;
border-left-width: 4px;
border-left-style: solid;
}
.date {
flex: 0 0 250px;
font-weight: bold;
}
.time {
flex: 0 0 120px;
margin-right: 10px;
border: none !important;
}
.movieTitle,
.genres {
@add-mixin truncate;
flex: 0 1 300px;
margin-right: 10px;
}
.statusIcon {
margin-left: 3px;
}
/*
* Status
*/
.downloaded {
composes: downloaded from '~Calendar/Events/CalendarEvent.css';
}
.downloading {
composes: downloading from '~Calendar/Events/CalendarEvent.css';
}
.unmonitored {
composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
}
.missing {
composes: missing from '~Calendar/Events/CalendarEvent.css';
}
.unreleased {
composes: unreleased from '~Calendar/Events/CalendarEvent.css';
}
@media only screen and (max-width: $breakpointSmall) {
.event {
flex-direction: column;
}
.eventWrapper {
display: block;
flex: 0 0 auto;
}
.date {
margin-left: 10px;
}
.date,
.time,
.movieTitle {
flex: 0 0 100%;
}
}
.dateIcon {
width: 25px;
}

View File

@@ -1,193 +0,0 @@
import classNames from 'classnames';
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
import getStatusStyle from 'Calendar/getStatusStyle';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './AgendaEvent.css';
class AgendaEvent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isDetailsModalOpen: false
};
}
//
// Listeners
onPress = () => {
this.setState({ isDetailsModalOpen: true });
}
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
}
//
// Render
render() {
const {
movieFile,
title,
titleSlug,
genres,
isAvailable,
inCinemas,
digitalRelease,
physicalRelease,
monitored,
hasFile,
grabbed,
queueItem,
showDate,
showMovieInformation,
showCutoffUnmetIcon,
longDateFormat,
colorImpairedMode,
cinemaDateParsed,
digitalDateParsed,
physicalDateParsed,
sortDate
} = this.props;
let startTime = null;
let releaseIcon = null;
if (physicalDateParsed === sortDate) {
startTime = physicalRelease;
releaseIcon = icons.DISC;
}
if (digitalDateParsed === sortDate) {
startTime = digitalRelease;
releaseIcon = icons.MOVIE_FILE;
}
if (cinemaDateParsed === sortDate) {
startTime = inCinemas;
releaseIcon = icons.IN_CINEMAS;
}
startTime = moment(startTime);
const downloading = !!(queueItem || grabbed);
const isMonitored = monitored;
const statusStyle = getStatusStyle(hasFile, downloading, isAvailable, isMonitored);
const joinedGenres = genres.slice(0, 2).join(', ');
const link = `/movie/${titleSlug}`;
return (
<div>
<Link
className={classNames(
styles.event,
styles.link
)}
to={link}
>
<div className={styles.dateIcon}>
<Icon
name={releaseIcon}
kind={kinds.DEFAULT}
/>
</div>
<div className={styles.date}>
{(showDate) ? startTime.format(longDateFormat) : null}
</div>
<div
className={classNames(
styles.eventWrapper,
styles[statusStyle],
colorImpairedMode && 'colorImpaired'
)}
>
<div className={styles.movieTitle}>
{title}
</div>
{
showMovieInformation &&
<div className={styles.genres}>
{joinedGenres}
</div>
}
{
!!queueItem &&
<span className={styles.statusIcon}>
<CalendarEventQueueDetails
{...queueItem}
/>
</span>
}
{
!queueItem && grabbed &&
<Icon
className={styles.statusIcon}
name={icons.DOWNLOADING}
title={translate('MovieIsDownloading')}
/>
}
{
showCutoffUnmetIcon &&
!!movieFile &&
movieFile.qualityCutoffNotMet &&
<Icon
className={styles.statusIcon}
name={icons.MOVIE_FILE}
kind={kinds.WARNING}
title={translate('QualityCutoffHasNotBeenMet')}
/>
}
</div>
</Link>
</div>
);
}
}
AgendaEvent.propTypes = {
id: PropTypes.number.isRequired,
movieFile: PropTypes.object,
title: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
isAvailable: PropTypes.bool.isRequired,
inCinemas: PropTypes.string,
digitalRelease: PropTypes.string,
physicalRelease: PropTypes.string,
monitored: PropTypes.bool.isRequired,
hasFile: PropTypes.bool.isRequired,
grabbed: PropTypes.bool,
queueItem: PropTypes.object,
showDate: PropTypes.bool.isRequired,
showMovieInformation: PropTypes.bool.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
colorImpairedMode: PropTypes.bool.isRequired,
cinemaDateParsed: PropTypes.number,
digitalDateParsed: PropTypes.number,
physicalDateParsed: PropTypes.number,
sortDate: PropTypes.number
};
AgendaEvent.defaultProps = {
genres: []
};
export default AgendaEvent;

View File

@@ -1,30 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import AgendaEvent from './AgendaEvent';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar.options,
createMovieSelector(),
createMovieFileSelector(),
createQueueItemSelector(),
createUISettingsSelector(),
(calendarOptions, movie, movieFile, queueItem, uiSettings) => {
return {
movie,
movieFile,
queueItem,
...calendarOptions,
timeFormat: uiSettings.timeFormat,
longDateFormat: uiSettings.longDateFormat,
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}
);
}
export default connect(createMapStateToProps)(AgendaEvent);

View File

@@ -1,8 +0,0 @@
.calendar {
flex-grow: 1;
width: 100%;
}
.calendarContent {
width: 100%;
}

View File

@@ -1,67 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import translate from 'Utilities/String/translate';
import AgendaConnector from './Agenda/AgendaConnector';
import * as calendarViews from './calendarViews';
import CalendarDaysConnector from './Day/CalendarDaysConnector';
import DaysOfWeekConnector from './Day/DaysOfWeekConnector';
import CalendarHeaderConnector from './Header/CalendarHeaderConnector';
import styles from './Calendar.css';
class Calendar extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
view
} = this.props;
return (
<div className={styles.calendar}>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>
{translate('UnableToLoadTheCalendar')}
</div>
}
{
!error && isPopulated && view === calendarViews.AGENDA &&
<div className={styles.calendarContent}>
<CalendarHeaderConnector />
<AgendaConnector />
</div>
}
{
!error && isPopulated && view !== calendarViews.AGENDA &&
<div className={styles.calendarContent}>
<CalendarHeaderConnector />
<DaysOfWeekConnector />
<CalendarDaysConnector />
</div>
}
</div>
);
}
}
Calendar.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
view: PropTypes.string.isRequired
};
export default Calendar;

View File

@@ -1,195 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import * as calendarActions from 'Store/Actions/calendarActions';
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Calendar from './Calendar';
const UPDATE_DELAY = 3600000; // 1 hour
function createMapStateToProps() {
return createSelector(
(state) => state.calendar,
(state) => state.settings.ui.item.firstDayOfWeek,
createCommandExecutingSelector(commandNames.REFRESH_MOVIE),
(calendar, firstDayOfWeek, isRefreshingMovie) => {
return {
...calendar,
isRefreshingMovie,
firstDayOfWeek
};
}
);
}
const mapDispatchToProps = {
...calendarActions,
fetchMovieFiles,
clearMovieFiles,
fetchQueueDetails,
clearQueueDetails
};
class CalendarConnector extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.updateTimeoutId = null;
}
componentDidMount() {
const {
useCurrentPage,
fetchCalendar,
gotoCalendarToday
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchCalendar();
} else {
gotoCalendarToday();
}
this.scheduleUpdate();
}
componentDidUpdate(prevProps) {
const {
items,
time,
view,
isRefreshingMovie,
firstDayOfWeek
} = this.props;
if (hasDifferentItems(prevProps.items, items)) {
const movieFileIds = selectUniqueIds(items, 'movieFileId');
if (movieFileIds.length) {
this.props.fetchMovieFiles({ movieFileIds });
}
if (items.length) {
this.props.fetchQueueDetails();
}
}
if (prevProps.time !== time) {
this.scheduleUpdate();
}
if (prevProps.firstDayOfWeek !== firstDayOfWeek) {
this.props.fetchCalendar({ time, view });
}
if (prevProps.isRefreshingMovie && !isRefreshingMovie) {
this.props.fetchCalendar({ time, view });
}
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
this.props.clearCalendar();
this.props.clearQueueDetails();
this.props.clearMovieFiles();
this.clearUpdateTimeout();
}
//
// Control
repopulate = () => {
const {
time,
view
} = this.props;
this.props.fetchQueueDetails({ time, view });
this.props.fetchCalendar({ time, view });
}
scheduleUpdate = () => {
this.clearUpdateTimeout();
this.updateTimeoutId = setTimeout(this.updateCalendar, UPDATE_DELAY);
}
clearUpdateTimeout = () => {
if (this.updateTimeoutId) {
clearTimeout(this.updateTimeoutId);
}
}
updateCalendar = () => {
this.props.gotoCalendarToday();
this.scheduleUpdate();
}
//
// Listeners
onCalendarViewChange = (view) => {
this.props.setCalendarView({ view });
}
onTodayPress = () => {
this.props.gotoCalendarToday();
}
onPreviousPress = () => {
this.props.gotoCalendarPreviousRange();
}
onNextPress = () => {
this.props.gotoCalendarNextRange();
}
//
// Render
render() {
return (
<Calendar
{...this.props}
onCalendarViewChange={this.onCalendarViewChange}
onTodayPress={this.onTodayPress}
onPreviousPress={this.onPreviousPress}
onNextPress={this.onNextPress}
/>
);
}
}
CalendarConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
time: PropTypes.string,
view: PropTypes.string.isRequired,
firstDayOfWeek: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
isRefreshingMovie: PropTypes.bool.isRequired,
setCalendarView: PropTypes.func.isRequired,
gotoCalendarToday: PropTypes.func.isRequired,
gotoCalendarPreviousRange: PropTypes.func.isRequired,
gotoCalendarNextRange: PropTypes.func.isRequired,
clearCalendar: PropTypes.func.isRequired,
fetchCalendar: PropTypes.func.isRequired,
fetchMovieFiles: PropTypes.func.isRequired,
clearMovieFiles: PropTypes.func.isRequired,
fetchQueueDetails: PropTypes.func.isRequired,
clearQueueDetails: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarConnector);

View File

@@ -1,20 +0,0 @@
.calendarPageBody {
composes: contentBody from '~Components/Page/PageContentBody.css';
display: flex;
}
.calendarInnerPageBody {
composes: innerContentBody from '~Components/Page/PageContentBody.css';
display: flex;
flex-direction: column;
flex-grow: 1;
width: 100%;
}
.errorMessage {
margin-top: 20px;
text-align: center;
font-size: 20px;
}

View File

@@ -1,220 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Measure from 'Components/Measure';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { align, icons } from 'Helpers/Props';
import NoMovie from 'Movie/NoMovie';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import CalendarConnector from './CalendarConnector';
import CalendarLinkModal from './iCal/CalendarLinkModal';
import LegendConnector from './Legend/LegendConnector';
import CalendarOptionsModal from './Options/CalendarOptionsModal';
import styles from './CalendarPage.css';
const MINIMUM_DAY_WIDTH = 120;
class CalendarPage extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isCalendarLinkModalOpen: false,
isOptionsModalOpen: false,
width: 0
};
}
//
// Listeners
onMeasure = ({ width }) => {
this.setState({ width });
const days = Math.max(3, Math.min(7, Math.floor(width / MINIMUM_DAY_WIDTH)));
this.props.onDaysCountChange(days);
}
onGetCalendarLinkPress = () => {
this.setState({ isCalendarLinkModalOpen: true });
}
onGetCalendarLinkModalClose = () => {
this.setState({ isCalendarLinkModalOpen: false });
}
onOptionsPress = () => {
this.setState({ isOptionsModalOpen: true });
}
onOptionsModalClose = () => {
this.setState({ isOptionsModalOpen: false });
}
onSearchMissingPress = () => {
const {
missingMovieIds,
onSearchMissingPress
} = this.props;
onSearchMissingPress(missingMovieIds);
}
//
// Render
render() {
const {
selectedFilterKey,
filters,
hasMovie,
movieError,
movieIsFetching,
movieIsPopulated,
missingMovieIds,
isRssSyncExecuting,
isSearchingForMissing,
useCurrentPage,
onRssSyncPress,
onFilterSelect
} = this.props;
const {
isCalendarLinkModalOpen,
isOptionsModalOpen
} = this.state;
const isMeasured = this.state.width > 0;
return (
<PageContent title={translate('Calendar')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('iCalLink')}
iconName={icons.CALENDAR}
onPress={this.onGetCalendarLinkPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('RSSSync')}
iconName={icons.RSS}
isSpinning={isRssSyncExecuting}
onPress={onRssSyncPress}
/>
<PageToolbarButton
label={translate('SearchForMissing')}
iconName={icons.SEARCH}
isDisabled={!missingMovieIds.length}
isSpinning={isSearchingForMissing}
onPress={this.onSearchMissingPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<PageToolbarButton
label={translate('Options')}
iconName={icons.POSTER}
onPress={this.onOptionsPress}
/>
<FilterMenu
alignMenu={align.RIGHT}
isDisabled={!hasMovie}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody
className={styles.calendarPageBody}
innerClassName={styles.calendarInnerPageBody}
>
{
movieIsFetching && !movieIsPopulated &&
<LoadingIndicator />
}
{
movieError &&
<div className={styles.errorMessage}>
{getErrorMessage(movieError, 'Failed to load movies from API')}
</div>
}
{
!movieError && movieIsPopulated && hasMovie &&
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
{
isMeasured ?
<CalendarConnector
useCurrentPage={useCurrentPage}
/> :
<div />
}
</Measure>
}
{
!movieError && movieIsPopulated && !hasMovie &&
<NoMovie />
}
{
hasMovie && !movieError &&
<LegendConnector />
}
</PageContentBody>
<CalendarLinkModal
isOpen={isCalendarLinkModalOpen}
onModalClose={this.onGetCalendarLinkModalClose}
/>
<CalendarOptionsModal
isOpen={isOptionsModalOpen}
onModalClose={this.onOptionsModalClose}
/>
</PageContent>
);
}
}
CalendarPage.propTypes = {
selectedFilterKey: PropTypes.string.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
hasMovie: PropTypes.bool.isRequired,
movieError: PropTypes.object,
movieIsFetching: PropTypes.bool.isRequired,
movieIsPopulated: PropTypes.bool.isRequired,
missingMovieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
isRssSyncExecuting: PropTypes.bool.isRequired,
isSearchingForMissing: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired,
onSearchMissingPress: PropTypes.func.isRequired,
onDaysCountChange: PropTypes.func.isRequired,
onRssSyncPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired
};
export default CalendarPage;

View File

@@ -1,116 +0,0 @@
import moment from 'moment';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createMovieCountSelector from 'Store/Selectors/createMovieCountSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { isCommandExecuting } from 'Utilities/Command';
import isBefore from 'Utilities/Date/isBefore';
import CalendarPage from './CalendarPage';
function createMissingMovieIdsSelector() {
return createSelector(
(state) => state.calendar.start,
(state) => state.calendar.end,
(state) => state.calendar.items,
(state) => state.queue.details.items,
(start, end, movies, queueDetails) => {
return movies.reduce((acc, movie) => {
const inCinemas = movie.inCinemas;
if (
!movie.movieFileId &&
moment(inCinemas).isAfter(start) &&
moment(inCinemas).isBefore(end) &&
isBefore(movie.inCinemas) &&
!queueDetails.some((details) => details.movieId === movie.id)
) {
acc.push(movie.id);
}
return acc;
}, []);
}
);
}
function createIsSearchingSelector() {
return createSelector(
(state) => state.calendar.searchMissingCommandId,
createCommandsSelector(),
(searchMissingCommandId, commands) => {
if (searchMissingCommandId == null) {
return false;
}
return isCommandExecuting(commands.find((command) => {
return command.id === searchMissingCommandId;
}));
}
);
}
function createMapStateToProps() {
return createSelector(
(state) => state.calendar.selectedFilterKey,
(state) => state.calendar.filters,
createMovieCountSelector(),
createUISettingsSelector(),
createMissingMovieIdsSelector(),
createCommandExecutingSelector(commandNames.RSS_SYNC),
createIsSearchingSelector(),
(
selectedFilterKey,
filters,
movieCount,
uiSettings,
missingMovieIds,
isRssSyncExecuting,
isSearchingForMissing
) => {
return {
selectedFilterKey,
filters,
colorImpairedMode: uiSettings.enableColorImpairedMode,
hasMovie: !!movieCount.count,
movieError: movieCount.error,
movieIsFetching: movieCount.isFetching,
movieIsPopulated: movieCount.isPopulated,
missingMovieIds,
isRssSyncExecuting,
isSearchingForMissing
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onRssSyncPress() {
dispatch(executeCommand({
name: commandNames.RSS_SYNC
}));
},
onSearchMissingPress(movieIds) {
dispatch(searchMissing({ movieIds }));
},
onDaysCountChange(dayCount) {
dispatch(setCalendarDaysCount({ dayCount }));
},
onFilterSelect(selectedFilterKey) {
dispatch(setCalendarFilter({ selectedFilterKey }));
}
};
}
export default withCurrentPage(
connect(createMapStateToProps, createMapDispatchToProps)(CalendarPage)
);

View File

@@ -1,25 +0,0 @@
.day {
flex: 1 0 14.28%;
overflow: hidden;
min-height: 70px;
border-bottom: 1px solid $calendarBorderColor;
border-left: 1px solid $calendarBorderColor;
}
.isSingleDay {
width: 100%;
}
.dayOfMonth {
padding-right: 5px;
border-bottom: 1px solid $calendarBorderColor;
text-align: right;
}
.isToday {
background-color: $calendarTodayBackgroundColor;
}
.isDifferentMonth {
color: $disabledColor;
}

View File

@@ -1,64 +0,0 @@
import classNames from 'classnames';
import moment from 'moment';
import PropTypes from 'prop-types';
import React from 'react';
import * as calendarViews from 'Calendar/calendarViews';
import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector';
import styles from './CalendarDay.css';
function CalendarDay(props) {
const {
date,
time,
isTodaysDate,
events,
view,
onEventModalOpenToggle
} = props;
return (
<div className={classNames(
styles.day,
view === calendarViews.DAY && styles.isSingleDay
)}
>
{
view === calendarViews.MONTH &&
<div className={classNames(
styles.dayOfMonth,
isTodaysDate && styles.isToday,
!moment(date).isSame(moment(time), 'month') && styles.isDifferentMonth
)}
>
{moment(date).date()}
</div>
}
<div>
{
events.map((event) => {
return (
<CalendarEventConnector
key={event.id}
movieId={event.id}
date={date}
{...event}
onEventModalOpenToggle={onEventModalOpenToggle}
/>
);
})
}
</div>
</div>
);
}
CalendarDay.propTypes = {
date: PropTypes.string.isRequired,
time: PropTypes.string.isRequired,
isTodaysDate: PropTypes.bool.isRequired,
events: PropTypes.arrayOf(PropTypes.object).isRequired,
view: PropTypes.string.isRequired,
onEventModalOpenToggle: PropTypes.func.isRequired
};
export default CalendarDay;

View File

@@ -1,67 +0,0 @@
import _ from 'lodash';
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import CalendarDay from './CalendarDay';
function sort(items) {
return _.sortBy(items, (item) => {
if (item.isGroup) {
return moment(item.events[0].inCinemas).unix();
}
return moment(item.inCinemas).unix();
});
}
function createCalendarEventsConnector() {
return createSelector(
(state, { date }) => date,
(state) => state.calendar.items,
(date, items) => {
const filtered = _.filter(items, (item) => {
return (item.inCinemas && moment(date).isSame(moment(item.inCinemas), 'day')) ||
(item.physicalRelease && moment(date).isSame(moment(item.physicalRelease), 'day')) ||
(item.digitalRelease && moment(date).isSame(moment(item.digitalRelease), 'day'));
});
return sort(filtered);
}
);
}
function createMapStateToProps() {
return createSelector(
(state) => state.calendar,
createCalendarEventsConnector(),
(calendar, events) => {
return {
time: calendar.time,
view: calendar.view,
events
};
}
);
}
class CalendarDayConnector extends Component {
//
// Render
render() {
return (
<CalendarDay
{...this.props}
/>
);
}
}
CalendarDayConnector.propTypes = {
date: PropTypes.string.isRequired
};
export default connect(createMapStateToProps)(CalendarDayConnector);

View File

@@ -1,14 +0,0 @@
.days {
display: flex;
border-right: 1px solid $calendarBorderColor;
}
.day,
.week,
.forecast {
flex-wrap: nowrap;
}
.month {
flex-wrap: wrap;
}

View File

@@ -1,164 +0,0 @@
import classNames from 'classnames';
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import * as calendarViews from 'Calendar/calendarViews';
import isToday from 'Utilities/Date/isToday';
import CalendarDayConnector from './CalendarDayConnector';
import styles from './CalendarDays.css';
class CalendarDays extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._touchStart = null;
this.state = {
todaysDate: moment().startOf('day').toISOString(),
isEventModalOpen: false
};
this.updateTimeoutId = null;
}
// Lifecycle
componentDidMount() {
const view = this.props.view;
if (view === calendarViews.MONTH) {
this.scheduleUpdate();
}
window.addEventListener('touchstart', this.onTouchStart);
window.addEventListener('touchend', this.onTouchEnd);
window.addEventListener('touchcancel', this.onTouchCancel);
window.addEventListener('touchmove', this.onTouchMove);
}
componentWillUnmount() {
this.clearUpdateTimeout();
window.removeEventListener('touchstart', this.onTouchStart);
window.removeEventListener('touchend', this.onTouchEnd);
window.removeEventListener('touchcancel', this.onTouchCancel);
window.removeEventListener('touchmove', this.onTouchMove);
}
//
// Control
scheduleUpdate = () => {
this.clearUpdateTimeout();
const todaysDate = moment().startOf('day');
const diff = moment().diff(todaysDate.clone().add(1, 'day'));
this.setState({ todaysDate: todaysDate.toISOString() });
this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff);
}
clearUpdateTimeout = () => {
if (this.updateTimeoutId) {
clearTimeout(this.updateTimeoutId);
}
}
//
// Listeners
onEventModalOpenToggle = (isEventModalOpen) => {
this.setState({ isEventModalOpen });
}
onTouchStart = (event) => {
const touches = event.touches;
const touchStart = touches[0].pageX;
if (touches.length !== 1) {
return;
}
if (
touchStart < 50 ||
this.props.isSidebarVisible ||
this.state.isEventModalOpen
) {
return;
}
this._touchStart = touchStart;
}
onTouchEnd = (event) => {
const touches = event.changedTouches;
const currentTouch = touches[0].pageX;
if (!this._touchStart) {
return;
}
if (currentTouch > this._touchStart && currentTouch - this._touchStart > 100) {
this.props.onNavigatePrevious();
} else if (currentTouch < this._touchStart && this._touchStart - currentTouch > 100) {
this.props.onNavigateNext();
}
this._touchStart = null;
}
onTouchCancel = (event) => {
this._touchStart = null;
}
onTouchMove = (event) => {
if (!this._touchStart) {
return;
}
}
//
// Render
render() {
const {
dates,
view
} = this.props;
return (
<div className={classNames(
styles.days,
styles[view]
)}
>
{
dates.map((date) => {
return (
<CalendarDayConnector
key={date}
date={date}
isTodaysDate={isToday(date)}
onEventModalOpenToggle={this.onEventModalOpenToggle}
/>
);
})
}
</div>
);
}
}
CalendarDays.propTypes = {
dates: PropTypes.arrayOf(PropTypes.string).isRequired,
view: PropTypes.string.isRequired,
isSidebarVisible: PropTypes.bool.isRequired,
onNavigatePrevious: PropTypes.func.isRequired,
onNavigateNext: PropTypes.func.isRequired
};
export default CalendarDays;

View File

@@ -1,25 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { gotoCalendarNextRange, gotoCalendarPreviousRange } from 'Store/Actions/calendarActions';
import CalendarDays from './CalendarDays';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar,
(state) => state.app.isSidebarVisible,
(calendar, isSidebarVisible) => {
return {
dates: calendar.dates,
view: calendar.view,
isSidebarVisible
};
}
);
}
const mapDispatchToProps = {
onNavigatePrevious: gotoCalendarPreviousRange,
onNavigateNext: gotoCalendarNextRange
};
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarDays);

View File

@@ -1,13 +0,0 @@
.dayOfWeek {
flex: 1 0 14.28%;
background-color: #e4eaec;
text-align: center;
}
.isSingleDay {
width: 100%;
}
.isToday {
background-color: $calendarTodayBackgroundColor;
}

View File

@@ -1,56 +0,0 @@
import classNames from 'classnames';
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import * as calendarViews from 'Calendar/calendarViews';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import styles from './DayOfWeek.css';
class DayOfWeek extends Component {
//
// Render
render() {
const {
date,
view,
isTodaysDate,
calendarWeekColumnHeader,
shortDateFormat,
showRelativeDates
} = this.props;
const highlightToday = view !== calendarViews.MONTH && isTodaysDate;
const momentDate = moment(date);
let formatedDate = momentDate.format('dddd');
if (view === calendarViews.WEEK) {
formatedDate = momentDate.format(calendarWeekColumnHeader);
} else if (view === calendarViews.FORECAST) {
formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates);
}
return (
<div className={classNames(
styles.dayOfWeek,
view === calendarViews.DAY && styles.isSingleDay,
highlightToday && styles.isToday
)}
>
{formatedDate}
</div>
);
}
}
DayOfWeek.propTypes = {
date: PropTypes.string.isRequired,
view: PropTypes.string.isRequired,
isTodaysDate: PropTypes.bool.isRequired,
calendarWeekColumnHeader: PropTypes.string.isRequired,
shortDateFormat: PropTypes.string.isRequired,
showRelativeDates: PropTypes.bool.isRequired
};
export default DayOfWeek;

View File

@@ -1,4 +0,0 @@
.daysOfWeek {
display: flex;
margin-top: 10px;
}

View File

@@ -1,97 +0,0 @@
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import * as calendarViews from 'Calendar/calendarViews';
import DayOfWeek from './DayOfWeek';
import styles from './DaysOfWeek.css';
class DaysOfWeek extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
todaysDate: moment().startOf('day').toISOString()
};
this.updateTimeoutId = null;
}
// Lifecycle
componentDidMount() {
const view = this.props.view;
if (view !== calendarViews.AGENDA || view !== calendarViews.MONTH) {
this.scheduleUpdate();
}
}
componentWillUnmount() {
this.clearUpdateTimeout();
}
//
// Control
scheduleUpdate = () => {
this.clearUpdateTimeout();
const todaysDate = moment().startOf('day');
const diff = todaysDate.clone().add(1, 'day').diff(moment());
this.setState({
todaysDate: todaysDate.toISOString()
});
this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff);
}
clearUpdateTimeout = () => {
if (this.updateTimeoutId) {
clearTimeout(this.updateTimeoutId);
}
}
//
// Render
render() {
const {
dates,
view,
...otherProps
} = this.props;
if (view === calendarViews.AGENDA) {
return null;
}
return (
<div className={styles.daysOfWeek}>
{
dates.map((date) => {
return (
<DayOfWeek
key={date}
date={date}
view={view}
isTodaysDate={date === this.state.todaysDate}
{...otherProps}
/>
);
})
}
</div>
);
}
}
DaysOfWeek.propTypes = {
dates: PropTypes.arrayOf(PropTypes.string),
view: PropTypes.string.isRequired
};
export default DaysOfWeek;

View File

@@ -1,22 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import DaysOfWeek from './DaysOfWeek';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar,
createUISettingsSelector(),
(calendar, UiSettings) => {
return {
dates: calendar.dates.slice(0, 7),
view: calendar.view,
calendarWeekColumnHeader: UiSettings.calendarWeekColumnHeader,
shortDateFormat: UiSettings.shortDateFormat,
showRelativeDates: UiSettings.showRelativeDates
};
}
);
}
export default connect(createMapStateToProps)(DaysOfWeek);

View File

@@ -1,98 +0,0 @@
.event {
overflow-x: hidden;
margin: 4px 2px;
padding: 5px;
border-bottom: 1px solid $borderColor;
border-left: 4px solid $borderColor;
font-size: 12px;
&:global(.colorImpaired) {
border-left-width: 5px;
}
}
.link {
composes: link from '~Components/Link/Link.css';
display: block;
color: $defaultColor;
&:hover {
color: $defaultColor;
text-decoration: none;
}
}
.info,
.movieInfo {
display: flex;
}
.movieInfo {
color: $calendarTextDim;
}
.movieTitle,
.genres {
@add-mixin truncate;
flex: 1 0 1px;
margin-right: 10px;
}
.movieTitle {
color: #3a3f51;
font-size: $defaultFontSize;
}
.statusIcon {
margin-left: 3px;
}
/*
* Status
*/
.downloaded {
border-left-color: $successColor !important;
&:global(.colorImpaired) {
border-left-color: color($successColor, saturation(+15%)) !important;
}
}
.downloading {
border-left-color: $purple !important;
}
.unmonitored {
border-left-color: $gray !important;
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
.onAir {
border-left-color: $warningColor !important;
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
.missing {
border-left-color: $dangerColor !important;
&:global(.colorImpaired) {
border-left-color: color($dangerColor saturation(+15%)) !important;
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
.unreleased {
border-left-color: $primaryColor !important;
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}

View File

@@ -1,159 +0,0 @@
import classNames from 'classnames';
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getStatusStyle from 'Calendar/getStatusStyle';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
import styles from './CalendarEvent.css';
class CalendarEvent extends Component {
//
// Render
render() {
const {
movieFile,
isAvailable,
inCinemas,
physicalRelease,
digitalRelease,
title,
titleSlug,
genres,
monitored,
certification,
hasFile,
grabbed,
queueItem,
showMovieInformation,
showCutoffUnmetIcon,
colorImpairedMode,
date
} = this.props;
const isDownloading = !!(queueItem || grabbed);
const isMonitored = monitored;
const statusStyle = getStatusStyle(hasFile, isDownloading, isAvailable, isMonitored);
const joinedGenres = genres.slice(0, 2).join(', ');
const link = `/movie/${titleSlug}`;
const eventType = [];
if (moment(date).isSame(moment(inCinemas), 'day')) {
eventType.push('Cinemas');
}
if (moment(date).isSame(moment(physicalRelease), 'day')) {
eventType.push('Physical');
}
if (moment(date).isSame(moment(digitalRelease), 'day')) {
eventType.push('Digital');
}
return (
<div>
<Link
className={classNames(
styles.event,
styles.link,
styles[statusStyle],
colorImpairedMode && 'colorImpaired'
)}
// component="div"
to={link}
>
<div className={styles.info}>
<div className={styles.movieTitle}>
{title}
</div>
{
!!queueItem &&
<span className={styles.statusIcon}>
<CalendarEventQueueDetails
{...queueItem}
/>
</span>
}
{
!queueItem && grabbed &&
<Icon
className={styles.statusIcon}
name={icons.DOWNLOADING}
title={translate('MovieIsDownloading')}
/>
}
{
showCutoffUnmetIcon &&
!!movieFile &&
movieFile.qualityCutoffNotMet &&
<Icon
className={styles.statusIcon}
name={icons.MOVIE_FILE}
kind={kinds.WARNING}
title={translate('QualityCutoffHasNotBeenMet')}
/>
}
</div>
{
showMovieInformation &&
<div className={styles.movieInfo}>
<div className={styles.genres}>
{joinedGenres}
</div>
</div>
}
{
showMovieInformation &&
<div className={styles.movieInfo}>
<div className={styles.genres}>
{eventType.join(', ')}
</div>
<div>
{certification}
</div>
</div>
}
</Link>
</div>
);
}
}
CalendarEvent.propTypes = {
id: PropTypes.number.isRequired,
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
movieFile: PropTypes.object,
title: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
isAvailable: PropTypes.bool.isRequired,
inCinemas: PropTypes.string,
physicalRelease: PropTypes.string,
digitalRelease: PropTypes.string,
monitored: PropTypes.bool.isRequired,
certification: PropTypes.string,
hasFile: PropTypes.bool.isRequired,
grabbed: PropTypes.bool,
queueItem: PropTypes.object,
showMovieInformation: PropTypes.bool.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired,
colorImpairedMode: PropTypes.bool.isRequired,
date: PropTypes.string.isRequired
};
CalendarEvent.defaultProps = {
genres: []
};
export default CalendarEvent;

View File

@@ -1,26 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import CalendarEvent from './CalendarEvent';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar.options,
createMovieSelector(),
createQueueItemSelector(),
createUISettingsSelector(),
(calendarOptions, movie, queueItem, uiSettings) => {
return {
movie,
queueItem,
...calendarOptions,
timeFormat: uiSettings.timeFormat,
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}
);
}
export default connect(createMapStateToProps)(CalendarEvent);

View File

@@ -1,57 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import QueueDetails from 'Activity/Queue/QueueDetails';
import CircularProgressBar from 'Components/CircularProgressBar';
import colors from 'Styles/Variables/colors';
import translate from 'Utilities/String/translate';
function CalendarEventQueueDetails(props) {
const {
title,
size,
sizeleft,
estimatedCompletionTime,
status,
trackedDownloadState,
trackedDownloadStatus,
errorMessage
} = props;
const progress = size ? (100 - sizeleft / size * 100) : 0;
return (
<QueueDetails
title={title}
size={size}
sizeleft={sizeleft}
estimatedCompletionTime={estimatedCompletionTime}
status={status}
trackedDownloadState={trackedDownloadState}
trackedDownloadStatus={trackedDownloadStatus}
errorMessage={errorMessage}
progressBar={
<div title={translate('MovieIsDownloadingInterp', [progress.toFixed(1), title])}>
<CircularProgressBar
progress={progress}
size={20}
strokeWidth={2}
strokeColor={colors.purple}
/>
</div>
}
/>
);
}
CalendarEventQueueDetails.propTypes = {
title: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
errorMessage: PropTypes.string
};
export default CalendarEventQueueDetails;

View File

@@ -1,53 +0,0 @@
.header {
display: flex;
}
.navigationButtons {
flex: 1 1 33%;
text-align: left;
}
.todayButton {
composes: button from '~Components/Link/Button.css';
margin-left: 5px;
}
.titleDesktop,
.titleMobile {
text-align: center;
font-size: 18px;
}
.titleMobile {
margin-bottom: 5px;
}
.viewButtonsContainer {
display: flex;
justify-content: flex-end;
flex: 1 1 33%;
}
.viewMenu {
composes: menu from '~Components/Menu/Menu.css';
line-height: 31px;
}
.loading {
composes: loading from '~Components/Loading/LoadingIndicator.css';
margin-top: 5px;
margin-right: 10px;
}
@media only screen and (max-width: $breakpointSmall) {
.navigationButtons {
flex: 1 0 50%;
}
.viewButtonsContainer {
flex: 0 0 100px;
}
}

View File

@@ -1,268 +0,0 @@
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import * as calendarViews from 'Calendar/calendarViews';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Menu from 'Components/Menu/Menu';
import MenuButton from 'Components/Menu/MenuButton';
import MenuContent from 'Components/Menu/MenuContent';
import ViewMenuItem from 'Components/Menu/ViewMenuItem';
import { align, icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import CalendarHeaderViewButton from './CalendarHeaderViewButton';
import styles from './CalendarHeader.css';
function getTitle(time, start, end, view, longDateFormat) {
const timeMoment = moment(time);
const startMoment = moment(start);
const endMoment = moment(end);
if (view === 'day') {
return timeMoment.format(longDateFormat);
} else if (view === 'month') {
return timeMoment.format('MMMM YYYY');
} else if (view === 'agenda') {
return `Agenda: ${startMoment.format('MMM D')} - ${endMoment.format('MMM D')}`;
}
let startFormat = 'MMM D YYYY';
let endFormat = 'MMM D YYYY';
if (startMoment.isSame(endMoment, 'month')) {
startFormat = 'MMM D';
endFormat = 'D YYYY';
} else if (startMoment.isSame(endMoment, 'year')) {
startFormat = 'MMM D';
endFormat = 'MMM D YYYY';
}
return `${startMoment.format(startFormat)} \u2014 ${endMoment.format(endFormat)}`;
}
// TODO Convert to a stateful Component so we can track view internally when changed
class CalendarHeader extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
view: props.view
};
}
componentDidUpdate(prevProps) {
const view = this.props.view;
if (prevProps.view !== view) {
this.setState({ view });
}
}
//
// Listeners
onViewChange = (view) => {
this.setState({ view }, () => {
this.props.onViewChange(view);
});
}
//
// Render
render() {
const {
isFetching,
time,
start,
end,
longDateFormat,
isSmallScreen,
collapseViewButtons,
onTodayPress,
onPreviousPress,
onNextPress
} = this.props;
const view = this.state.view;
const title = getTitle(time, start, end, view, longDateFormat);
return (
<div>
{
isSmallScreen &&
<div className={styles.titleMobile}>
{title}
</div>
}
<div className={styles.header}>
<div className={styles.navigationButtons}>
<Button
buttonGroupPosition={align.LEFT}
isDisabled={view === calendarViews.AGENDA}
onPress={onPreviousPress}
>
<Icon name={icons.PAGE_PREVIOUS} />
</Button>
<Button
buttonGroupPosition={align.RIGHT}
isDisabled={view === calendarViews.AGENDA}
onPress={onNextPress}
>
<Icon name={icons.PAGE_NEXT} />
</Button>
<Button
className={styles.todayButton}
isDisabled={view === calendarViews.AGENDA}
onPress={onTodayPress}
>
Today
</Button>
</div>
{
!isSmallScreen &&
<div className={styles.titleDesktop}>
{title}
</div>
}
<div className={styles.viewButtonsContainer}>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
{
collapseViewButtons ?
<Menu
className={styles.viewMenu}
alignMenu={align.RIGHT}
>
<MenuButton>
<Icon
name={icons.VIEW}
size={22}
/>
</MenuButton>
<MenuContent>
{
isSmallScreen ?
null :
<ViewMenuItem
name={calendarViews.MONTH}
selectedView={view}
onPress={this.onViewChange}
>
Month
</ViewMenuItem>
}
<ViewMenuItem
name={calendarViews.WEEK}
selectedView={view}
onPress={this.onViewChange}
>
Week
</ViewMenuItem>
<ViewMenuItem
name={calendarViews.FORECAST}
selectedView={view}
onPress={this.onViewChange}
>
Forecast
</ViewMenuItem>
<ViewMenuItem
name={calendarViews.DAY}
selectedView={view}
onPress={this.onViewChange}
>
{translate('Day')}
</ViewMenuItem>
<ViewMenuItem
name={calendarViews.AGENDA}
selectedView={view}
onPress={this.onViewChange}
>
{translate('Agenda')}
</ViewMenuItem>
</MenuContent>
</Menu> :
<div className={styles.viewButtons}>
<CalendarHeaderViewButton
view={calendarViews.MONTH}
selectedView={view}
buttonGroupPosition={align.LEFT}
onPress={this.onViewChange}
/>
<CalendarHeaderViewButton
view={calendarViews.WEEK}
selectedView={view}
buttonGroupPosition={align.CENTER}
onPress={this.onViewChange}
/>
<CalendarHeaderViewButton
view={calendarViews.FORECAST}
selectedView={view}
buttonGroupPosition={align.CENTER}
onPress={this.onViewChange}
/>
<CalendarHeaderViewButton
view={calendarViews.DAY}
selectedView={view}
buttonGroupPosition={align.CENTER}
onPress={this.onViewChange}
/>
<CalendarHeaderViewButton
view={calendarViews.AGENDA}
selectedView={view}
buttonGroupPosition={align.RIGHT}
onPress={this.onViewChange}
/>
</div>
}
</div>
</div>
</div>
);
}
}
CalendarHeader.propTypes = {
isFetching: PropTypes.bool.isRequired,
time: PropTypes.string.isRequired,
start: PropTypes.string.isRequired,
end: PropTypes.string.isRequired,
view: PropTypes.oneOf(calendarViews.all).isRequired,
isSmallScreen: PropTypes.bool.isRequired,
collapseViewButtons: PropTypes.bool.isRequired,
longDateFormat: PropTypes.string.isRequired,
onViewChange: PropTypes.func.isRequired,
onTodayPress: PropTypes.func.isRequired,
onPreviousPress: PropTypes.func.isRequired,
onNextPress: PropTypes.func.isRequired
};
export default CalendarHeader;

View File

@@ -1,81 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { gotoCalendarNextRange, gotoCalendarPreviousRange, gotoCalendarToday, setCalendarView } from 'Store/Actions/calendarActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import CalendarHeader from './CalendarHeader';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar,
createDimensionsSelector(),
createUISettingsSelector(),
(calendar, dimensions, uiSettings) => {
return {
isFetching: calendar.isFetching,
view: calendar.view,
time: calendar.time,
start: calendar.start,
end: calendar.end,
isSmallScreen: dimensions.isSmallScreen,
collapseViewButtons: dimensions.isLargeScreen,
longDateFormat: uiSettings.longDateFormat
};
}
);
}
const mapDispatchToProps = {
setCalendarView,
gotoCalendarToday,
gotoCalendarPreviousRange,
gotoCalendarNextRange
};
class CalendarHeaderConnector extends Component {
//
// Listeners
onViewChange = (view) => {
this.props.setCalendarView({ view });
}
onTodayPress = () => {
this.props.gotoCalendarToday();
}
onPreviousPress = () => {
this.props.gotoCalendarPreviousRange();
}
onNextPress = () => {
this.props.gotoCalendarNextRange();
}
//
// Render
render() {
return (
<CalendarHeader
{...this.props}
onViewChange={this.onViewChange}
onTodayPress={this.onTodayPress}
onPreviousPress={this.onPreviousPress}
onNextPress={this.onNextPress}
/>
);
}
}
CalendarHeaderConnector.propTypes = {
setCalendarView: PropTypes.func.isRequired,
gotoCalendarToday: PropTypes.func.isRequired,
gotoCalendarPreviousRange: PropTypes.func.isRequired,
gotoCalendarNextRange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarHeaderConnector);

View File

@@ -1,45 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import * as calendarViews from 'Calendar/calendarViews';
import Button from 'Components/Link/Button';
import titleCase from 'Utilities/String/titleCase';
// import styles from './CalendarHeaderViewButton.css';
class CalendarHeaderViewButton extends Component {
//
// Listeners
onPress = () => {
this.props.onPress(this.props.view);
}
//
// Render
render() {
const {
view,
selectedView,
...otherProps
} = this.props;
return (
<Button
isDisabled={selectedView === view}
{...otherProps}
onPress={this.onPress}
>
{titleCase(view)}
</Button>
);
}
}
CalendarHeaderViewButton.propTypes = {
view: PropTypes.oneOf(calendarViews.all).isRequired,
selectedView: PropTypes.oneOf(calendarViews.all).isRequired,
onPress: PropTypes.func.isRequired
};
export default CalendarHeaderViewButton;

View File

@@ -1,6 +0,0 @@
.legend {
display: flex;
flex-wrap: wrap;
margin-top: 10px;
padding: 3px 0;
}

View File

@@ -1,72 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { icons, kinds } from 'Helpers/Props';
import LegendIconItem from './LegendIconItem';
import LegendItem from './LegendItem';
import styles from './Legend.css';
function Legend(props) {
const {
showCutoffUnmetIcon,
colorImpairedMode
} = props;
const iconsToShow = [];
if (showCutoffUnmetIcon) {
iconsToShow.push(
<LegendIconItem
name="Cutoff Not Met"
icon={icons.MOVIE_FILE}
kind={kinds.WARNING}
tooltip="Quality or language cutoff has not been met"
/>
);
}
return (
<div className={styles.legend}>
<div>
<LegendItem
status="unreleased"
tooltip="Movie hasn't released yet"
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
status="unmonitored"
tooltip="Movie is unmonitored"
colorImpairedMode={colorImpairedMode}
/>
</div>
<div>
<LegendItem
status="downloading"
tooltip="Movie is currently downloading"
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
status="downloaded"
tooltip="Movie was downloaded and sorted"
colorImpairedMode={colorImpairedMode}
/>
</div>
{
iconsToShow.length > 0 &&
<div>
{iconsToShow[0]}
</div>
}
</div>
);
}
Legend.propTypes = {
showCutoffUnmetIcon: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};
export default Legend;

View File

@@ -1,19 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import Legend from './Legend';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar.options,
createUISettingsSelector(),
(calendarOptions, uiSettings) => {
return {
...calendarOptions,
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}
);
}
export default connect(createMapStateToProps)(Legend);

View File

@@ -1,10 +0,0 @@
.legendIconItem {
margin: 3px 0;
margin-right: 6px;
width: 150px;
cursor: default;
}
.icon {
margin-right: 5px;
}

View File

@@ -1,37 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import styles from './LegendIconItem.css';
function LegendIconItem(props) {
const {
name,
icon,
kind,
tooltip
} = props;
return (
<div
className={styles.legendIconItem}
title={tooltip}
>
<Icon
className={styles.icon}
name={icon}
kind={kind}
/>
{name}
</div>
);
}
LegendIconItem.propTypes = {
name: PropTypes.string.isRequired,
icon: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired
};
export default LegendIconItem;

View File

@@ -1,33 +0,0 @@
.legendItem {
margin: 3px 0;
margin-right: 6px;
padding-left: 5px;
width: 150px;
border-left-width: 4px;
border-left-style: solid;
cursor: default;
}
/*
* Status
*/
.downloaded {
composes: downloaded from '~Calendar/Events/CalendarEvent.css';
}
.downloading {
composes: downloading from '~Calendar/Events/CalendarEvent.css';
}
.unmonitored {
composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
}
.missing {
composes: missing from '~Calendar/Events/CalendarEvent.css';
}
.unreleased {
composes: unreleased from '~Calendar/Events/CalendarEvent.css';
}

View File

@@ -1,36 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import titleCase from 'Utilities/String/titleCase';
import styles from './LegendItem.css';
function LegendItem(props) {
const {
name,
status,
tooltip,
colorImpairedMode
} = props;
return (
<div
className={classNames(
styles.legendItem,
styles[status],
colorImpairedMode && 'colorImpaired'
)}
title={tooltip}
>
{name ? name : titleCase(status)}
</div>
);
}
LegendItem.propTypes = {
name: PropTypes.string,
status: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};
export default LegendItem;

View File

@@ -1,29 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import CalendarOptionsModalContentConnector from './CalendarOptionsModalContentConnector';
function CalendarOptionsModal(props) {
const {
isOpen,
onModalClose
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<CalendarOptionsModalContentConnector
onModalClose={onModalClose}
/>
</Modal>
);
}
CalendarOptionsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default CalendarOptionsModal;

View File

@@ -1,217 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props';
import { firstDayOfWeekOptions, timeFormatOptions, weekColumnOptions } from 'Settings/UI/UISettings';
import translate from 'Utilities/String/translate';
class CalendarOptionsModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
} = props;
this.state = {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
};
}
componentDidUpdate(prevProps) {
const {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
} = this.props;
if (
prevProps.firstDayOfWeek !== firstDayOfWeek ||
prevProps.calendarWeekColumnHeader !== calendarWeekColumnHeader ||
prevProps.timeFormat !== timeFormat ||
prevProps.enableColorImpairedMode !== enableColorImpairedMode
) {
this.setState({
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
});
}
}
//
// Listeners
onOptionInputChange = ({ name, value }) => {
const {
dispatchSetCalendarOption
} = this.props;
dispatchSetCalendarOption({ [name]: value });
}
onGlobalInputChange = ({ name, value }) => {
const {
dispatchSaveUISettings
} = this.props;
const setting = { [name]: value };
this.setState(setting, () => {
dispatchSaveUISettings(setting);
});
}
onLinkFocus = (event) => {
event.target.select();
}
//
// Render
render() {
const {
showMovieInformation,
showCutoffUnmetIcon,
onModalClose
} = this.props;
const {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Calendar Options
</ModalHeader>
<ModalBody>
<FieldSet legend={translate('Local')}>
<Form>
<FormGroup>
<FormLabel>{translate('ShowMovieInformation')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showMovieInformation"
value={showMovieInformation}
helpText={translate('ShowMovieInformationHelpText')}
onChange={this.onOptionInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('IconForCutoffUnmet')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showCutoffUnmetIcon"
value={showCutoffUnmetIcon}
helpText={translate('ShowCutoffUnmetIconHelpText')}
onChange={this.onOptionInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
<FieldSet legend={translate('Global')}>
<Form>
<FormGroup>
<FormLabel>{translate('FirstDayOfWeek')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="firstDayOfWeek"
values={firstDayOfWeekOptions}
value={firstDayOfWeek}
onChange={this.onGlobalInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('WeekColumnHeader')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="calendarWeekColumnHeader"
values={weekColumnOptions}
value={calendarWeekColumnHeader}
onChange={this.onGlobalInputChange}
helpText={translate('HelpText')}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('TimeFormat')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="timeFormat"
values={timeFormatOptions}
value={timeFormat}
onChange={this.onGlobalInputChange}
/>
</FormGroup><FormGroup>
<FormLabel>{translate('EnableColorImpairedMode')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableColorImpairedMode"
value={enableColorImpairedMode}
helpText={translate('EnableColorImpairedModeHelpText')}
onChange={this.onGlobalInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
CalendarOptionsModalContent.propTypes = {
showMovieInformation: PropTypes.bool.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
firstDayOfWeek: PropTypes.number.isRequired,
calendarWeekColumnHeader: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
enableColorImpairedMode: PropTypes.bool.isRequired,
dispatchSetCalendarOption: PropTypes.func.isRequired,
dispatchSaveUISettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default CalendarOptionsModalContent;

View File

@@ -1,25 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setCalendarOption } from 'Store/Actions/calendarActions';
import { saveUISettings } from 'Store/Actions/settingsActions';
import CalendarOptionsModalContent from './CalendarOptionsModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar.options,
(state) => state.settings.ui.item,
(options, uiSettings) => {
return {
...options,
...uiSettings
};
}
);
}
const mapDispatchToProps = {
dispatchSetCalendarOption: setCalendarOption,
dispatchSaveUISettings: saveUISettings
};
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarOptionsModalContent);

View File

@@ -1,7 +0,0 @@
export const DAY = 'day';
export const WEEK = 'week';
export const MONTH = 'month';
export const FORECAST = 'forecast';
export const AGENDA = 'agenda';
export const all = [DAY, WEEK, MONTH, FORECAST, AGENDA];

View File

@@ -1,23 +0,0 @@
function getStatusStyle(hasFile, downloading, isAvailable, isMonitored) {
if (hasFile) {
return 'downloaded';
}
if (downloading) {
return 'downloading';
}
if (!isMonitored) {
return 'unmonitored';
}
if (isAvailable && !hasFile) {
return 'missing';
}
return 'unreleased';
}
export default getStatusStyle;

View File

@@ -1,29 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import CalendarLinkModalContentConnector from './CalendarLinkModalContentConnector';
function CalendarLinkModal(props) {
const {
isOpen,
onModalClose
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<CalendarLinkModalContentConnector
onModalClose={onModalClose}
/>
</Modal>
);
}
CalendarLinkModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default CalendarLinkModal;

View File

@@ -1,203 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputButton from 'Components/Form/FormInputButton';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import ClipboardButton from 'Components/Link/ClipboardButton';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { icons, inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function getUrls(state) {
const {
unmonitored,
asAllDay,
tags
} = state;
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/calendar/Radarr.ics?`;
if (unmonitored) {
icalUrl += 'unmonitored=true&';
}
if (asAllDay) {
icalUrl += 'asAllDay=true&';
}
if (tags.length) {
icalUrl += `tags=${tags.toString()}&`;
}
icalUrl += `apikey=${window.Radarr.apiKey}`;
const iCalHttpUrl = `${window.location.protocol}//${icalUrl}`;
const iCalWebCalUrl = `webcal://${icalUrl}`;
return {
iCalHttpUrl,
iCalWebCalUrl
};
}
class CalendarLinkModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const defaultState = {
unmonitored: false,
asAllDay: false,
tags: []
};
const urls = getUrls(defaultState);
this.state = {
...defaultState,
...urls
};
}
//
// Listeners
onInputChange = ({ name, value }) => {
const state = {
...this.state,
[name]: value
};
const urls = getUrls(state);
this.setState({
[name]: value,
...urls
});
}
onLinkFocus = (event) => {
event.target.select();
}
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
unmonitored,
asAllDay,
tags,
iCalHttpUrl,
iCalWebCalUrl
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Radarr Calendar Feed
</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>{translate('IncludeUnmonitored')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="unmonitored"
value={unmonitored}
helpText={translate('UnmonitoredHelpText')}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ShowAsAllDayEvents')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="asAllDay"
value={asAllDay}
helpText={translate('AsAllDayHelpText')}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
value={tags}
helpText={translate('TagsHelpText')}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup
size={sizes.LARGE}
>
<FormLabel>{translate('ICalFeed')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="iCalHttpUrl"
value={iCalHttpUrl}
readOnly={true}
helpText={translate('ICalHttpUrlHelpText')}
buttons={[
<ClipboardButton
key="copy"
value={iCalHttpUrl}
kind={kinds.DEFAULT}
/>,
<FormInputButton
key="webcal"
kind={kinds.DEFAULT}
to={iCalWebCalUrl}
target="_blank"
noRouter={true}
>
<Icon name={icons.CALENDAR_O} />
</FormInputButton>
]}
onChange={this.onInputChange}
onFocus={this.onLinkFocus}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
CalendarLinkModalContent.propTypes = {
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired
};
export default CalendarLinkModalContent;

View File

@@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import CalendarLinkModalContent from './CalendarLinkModalContent';
function createMapStateToProps() {
return createSelector(
createTagsSelector(),
(tagList) => {
return {
tagList
};
}
);
}
export default connect(createMapStateToProps)(CalendarLinkModalContent);

View File

@@ -21,7 +21,7 @@ function ErrorBoundaryError(props) {
<div className={styles.imageContainer}>
<img
className={styles.image}
src={`${window.Radarr.urlBase}/Content/Images/error.png`}
src={`${window.Prowlarr.urlBase}/Content/Images/error.png`}
/>
</div>

View File

@@ -129,7 +129,7 @@ class FileBrowserModalContent extends Component {
className={styles.mappedDrivesWarning}
kind={kinds.WARNING}
>
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://github.com/Radarr/Radarr/wiki/FAQ">FAQ</Link> for more information.
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://github.com/Prowlarr/Prowlarr/wiki/FAQ">FAQ</Link> for more information.
</Alert>
}

View File

@@ -6,7 +6,6 @@ import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Prop
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
import ImportListFilterBuilderRowValueConnector from './ImportListFilterBuilderRowValueConnector';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import MovieStatusFilterBuilderRowValue from './MovieStatusFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
@@ -75,9 +74,6 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.TAG:
return TagFilterBuilderRowValueConnector;
case filterBuilderValueTypes.IMPORTLIST:
return ImportListFilterBuilderRowValueConnector;
default:
return FilterBuilderRowValueConnector;
}

View File

@@ -1,27 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createImportListSelector from 'Store/Selectors/createImportListSelector';
import FilterBuilderRowValue from './FilterBuilderRowValue';
function createMapStateToProps() {
return createSelector(
createImportListSelector(),
(importLists) => {
return {
tagList: importLists.map((importList) => {
const {
id,
name
} = importList;
return {
id,
name
};
})
};
}
);
}
export default connect(createMapStateToProps)(FilterBuilderRowValue);

View File

@@ -47,13 +47,13 @@ class Link extends Component {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_self';
} else if (to.startsWith(`${window.Radarr.urlBase}/`)) {
} else if (to.startsWith(`${window.Prowlarr.urlBase}/`)) {
el = RouterLink;
linkProps.to = to;
linkProps.target = target;
} else {
el = RouterLink;
linkProps.to = `${window.Radarr.urlBase}/${to.replace(/^\//, '')}`;
linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
linkProps.target = target;
}
}

View File

@@ -4,7 +4,7 @@ import styles from './LoadingMessage.css';
const messages = [
'Downloading more RAM',
'Now in Technicolor',
'Previously on Radarr...',
'Previously on Prowlarr...',
'Bleep Bloop.',
'Locating the required gigapixels to render...',
'Spinning up the hamster wheel...',

View File

@@ -14,7 +14,7 @@ function NotFound({ message }) {
<img
className={styles.image}
src={`${window.Radarr.urlBase}/Content/Images/404.png`}
src={`${window.Prowlarr.urlBase}/Content/Images/404.png`}
/>
</div>
</PageContent>

View File

@@ -16,7 +16,7 @@ function ErrorPage(props) {
systemStatusError
} = props;
let errorMessage = 'Failed to load Radarr';
let errorMessage = 'Failed to load Prowlarr';
if (!isLocalStorageSupported) {
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';

View File

@@ -59,11 +59,11 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
onGoToMovie(titleSlug) {
dispatch(push(`${window.Radarr.urlBase}/movie/${titleSlug}`));
dispatch(push(`${window.Prowlarr.urlBase}/movie/${titleSlug}`));
},
onGoToAddNewMovie(query) {
dispatch(push(`${window.Radarr.urlBase}/add/new?term=${encodeURIComponent(query)}`));
dispatch(push(`${window.Prowlarr.urlBase}/add/new?term=${encodeURIComponent(query)}`));
}
};
}

View File

@@ -53,10 +53,10 @@ class PageHeader extends Component {
return (
<div className={styles.header}>
<div className={styles.logoContainer}>
<Link to={`${window.Radarr.urlBase}/`}>
<Link to={`${window.Prowlarr.urlBase}/`}>
<img
className={isSmallScreen ? styles.logo : styles.logoFull}
src={isSmallScreen ? `${window.Radarr.urlBase}/Content/Images/logo.png` : `${window.Radarr.urlBase}/Content/Images/logo-full.png`}
src={isSmallScreen ? `${window.Prowlarr.urlBase}/Content/Images/logo.png` : `${window.Prowlarr.urlBase}/Content/Images/logo-full.png`}
/>
</Link>
</div>
@@ -75,14 +75,14 @@ class PageHeader extends Component {
<IconButton
className={styles.donate}
name={icons.HEART}
to="https://opencollective.com/radarr"
to="https://opencollective.com/prowlarr"
size={14}
/>
<IconButton
className={styles.translate}
title={translate('SuggestTranslationChange')}
name={icons.TRANSLATE}
to="https://translate.servarr.com/projects/radarr/radarr/"
to="https://translate.servarr.com/projects/prowlarr/prowlarr/"
size={24}
/>
<PageHeaderActionsMenuConnector

View File

@@ -63,7 +63,7 @@ function PageHeaderActionsMenu(props) {
{
formsAuth &&
<MenuItem
to={`${window.Radarr.urlBase}/logout`}
to={`${window.Prowlarr.urlBase}/logout`}
noRouter={true}
>
<Icon

View File

@@ -5,8 +5,7 @@ import { withRouter } from 'react-router-dom';
import { createSelector } from 'reselect';
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchMovies } from 'Store/Actions/movieActions';
import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
import { fetchIndexerFlags, fetchLanguages, fetchUISettings } from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@@ -46,29 +45,23 @@ const selectIsPopulated = createSelector(
(state) => state.customFilters.isPopulated,
(state) => state.tags.isPopulated,
(state) => state.settings.ui.isPopulated,
(state) => state.settings.qualityProfiles.isPopulated,
(state) => state.settings.languages.isPopulated,
(state) => state.settings.indexerFlags.isPopulated,
(state) => state.settings.importLists.isPopulated,
(state) => state.system.status.isPopulated,
(
customFiltersIsPopulated,
tagsIsPopulated,
uiSettingsIsPopulated,
qualityProfilesIsPopulated,
languagesIsPopulated,
indexerFlagsIsPopulated,
importListsIsPopulated,
systemStatusIsPopulated
) => {
return (
customFiltersIsPopulated &&
tagsIsPopulated &&
uiSettingsIsPopulated &&
qualityProfilesIsPopulated &&
languagesIsPopulated &&
indexerFlagsIsPopulated &&
importListsIsPopulated &&
systemStatusIsPopulated
);
}
@@ -78,29 +71,23 @@ const selectErrors = createSelector(
(state) => state.customFilters.error,
(state) => state.tags.error,
(state) => state.settings.ui.error,
(state) => state.settings.qualityProfiles.error,
(state) => state.settings.languages.error,
(state) => state.settings.indexerFlags.error,
(state) => state.settings.importLists.error,
(state) => state.system.status.error,
(
customFiltersError,
tagsError,
uiSettingsError,
qualityProfilesError,
languagesError,
indexerFlagsError,
importListsError,
systemStatusError
) => {
const hasError = !!(
customFiltersError ||
tagsError ||
uiSettingsError ||
qualityProfilesError ||
languagesError ||
indexerFlagsError ||
importListsError ||
systemStatusError
);
@@ -109,10 +96,8 @@ const selectErrors = createSelector(
customFiltersError,
tagsError,
uiSettingsError,
qualityProfilesError,
languagesError,
indexerFlagsError,
importListsError,
systemStatusError
};
}
@@ -145,27 +130,18 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
dispatchFetchMovies() {
dispatch(fetchMovies());
},
dispatchFetchCustomFilters() {
dispatch(fetchCustomFilters());
},
dispatchFetchTags() {
dispatch(fetchTags());
},
dispatchFetchQualityProfiles() {
dispatch(fetchQualityProfiles());
},
dispatchFetchLanguages() {
dispatch(fetchLanguages());
},
dispatchFetchIndexerFlags() {
dispatch(fetchIndexerFlags());
},
dispatchFetchImportLists() {
dispatch(fetchImportLists());
},
dispatchFetchUISettings() {
dispatch(fetchUISettings());
},
@@ -196,13 +172,10 @@ class PageConnector extends Component {
componentDidMount() {
if (!this.props.isPopulated) {
this.props.dispatchFetchMovies();
this.props.dispatchFetchCustomFilters();
this.props.dispatchFetchTags();
this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchLanguages();
this.props.dispatchFetchIndexerFlags();
this.props.dispatchFetchImportLists();
this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus();
}
@@ -222,12 +195,9 @@ class PageConnector extends Component {
const {
isPopulated,
hasError,
dispatchFetchMovies,
dispatchFetchTags,
dispatchFetchQualityProfiles,
dispatchFetchLanguages,
dispatchFetchIndexerFlags,
dispatchFetchImportLists,
dispatchFetchUISettings,
dispatchFetchStatus,
...otherProps
@@ -261,13 +231,10 @@ PageConnector.propTypes = {
isPopulated: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired,
dispatchFetchMovies: PropTypes.func.isRequired,
dispatchFetchCustomFilters: PropTypes.func.isRequired,
dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchLanguages: PropTypes.func.isRequired,
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired

View File

@@ -14,7 +14,7 @@ function PageContent(props) {
return (
<ErrorBoundary errorComponent={PageContentError}>
<DocumentTitle title={title ? `${title} - Radarr` : 'Radarr'}>
<DocumentTitle title={title ? `${title} - Prowlarr` : 'Prowlarr'}>
<div className={className}>
{children}
</div>

View File

@@ -21,7 +21,7 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [
{
iconName: icons.MOVIE_CONTINUING,
title: translate('Movies'),
title: 'Indexers',
to: '/',
alias: '/movies',
children: [
@@ -32,23 +32,13 @@ const links = [
{
title: translate('Import'),
to: '/add/import'
},
{
title: translate('Discover'),
to: '/add/discover'
}
]
},
{
iconName: icons.CALENDAR,
title: translate('Calendar'),
to: '/calendar'
},
{
iconName: icons.ACTIVITY,
title: translate('Activity'),
title: 'Search',
to: '/activity/queue',
children: [
{
@@ -72,42 +62,14 @@ const links = [
title: translate('Settings'),
to: '/settings',
children: [
{
title: translate('MediaManagement'),
to: '/settings/mediamanagement'
},
{
title: translate('Profiles'),
to: '/settings/profiles'
},
{
title: translate('Quality'),
to: '/settings/quality'
},
{
title: translate('CustomFormats'),
to: '/settings/customformats'
},
{
title: translate('Indexers'),
to: '/settings/indexers'
},
{
title: translate('DownloadClients'),
to: '/settings/downloadclients'
},
{
title: translate('Lists'),
to: '/settings/importlists'
},
{
title: translate('Connect'),
to: '/settings/connect'
},
{
title: translate('Metadata'),
to: '/settings/metadata'
},
{
title: translate('Tags'),
to: '/settings/tags'
@@ -415,7 +377,7 @@ class PageSidebar extends Component {
transform
} = this.state;
const urlBase = window.Radarr.urlBase;
const urlBase = window.Prowlarr.urlBase;
const pathname = urlBase ? location.pathname.substr(urlBase.length) || '/' : location.pathname;
const activeParent = getActiveParent(pathname);

View File

@@ -59,7 +59,7 @@ function Logger(minimumLogLevel) {
}
Logger.prototype.cleanse = function(message) {
const apikey = new RegExp(`access_token=${window.Radarr.apiKey}`, 'g');
const apikey = new RegExp(`access_token=${window.Prowlarr.apiKey}`, 'g');
return message.replace(apikey, 'access_token=(removed)');
};
@@ -99,11 +99,11 @@ class SignalRConnector extends Component {
componentDidMount() {
console.log('[signalR] starting');
const url = `${window.Radarr.urlBase}/signalr/messages`;
const url = `${window.Prowlarr.urlBase}/signalr/messages`;
this.connection = new signalR.HubConnectionBuilder()
.configureLogging(new Logger(signalR.LogLevel.Information))
.withUrl(`${url}?access_token=${window.Radarr.apiKey}`)
.withUrl(`${url}?access_token=${window.Prowlarr.apiKey}`)
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
if (retryContext.elapsedMilliseconds > 180000) {

View File

@@ -1,31 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddNewDiscoverMovieModalContentConnector from './AddNewDiscoverMovieModalContentConnector';
function AddNewDiscoverMovieModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<AddNewDiscoverMovieModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
AddNewDiscoverMovieModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddNewDiscoverMovieModal;

View File

@@ -1,105 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import AddNewMovieModalContent from 'AddMovie/AddNewMovie/AddNewMovieModalContent';
import { addMovie, setAddMovieDefault } from 'Store/Actions/discoverMovieActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import selectSettings from 'Store/Selectors/selectSettings';
function createMapStateToProps() {
return createSelector(
(state) => state.discoverMovie,
createDimensionsSelector(),
createSystemStatusSelector(),
(discoverMovieState, dimensions, systemStatus) => {
const {
isAdding,
addError,
defaults
} = discoverMovieState;
const {
settings,
validationErrors,
validationWarnings
} = selectSettings(defaults, {}, addError);
return {
isAdding,
addError,
isSmallScreen: dimensions.isSmallScreen,
validationErrors,
validationWarnings,
isWindows: systemStatus.isWindows,
...settings
};
}
);
}
const mapDispatchToProps = {
setAddMovieDefault,
addMovie
};
class AddNewDiscoverMovieModalContentConnector extends Component {
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setAddMovieDefault({ [name]: value });
}
onAddMoviePress = (searchForMovie) => {
const {
tmdbId,
rootFolderPath,
monitor,
qualityProfileId,
minimumAvailability,
tags
} = this.props;
this.props.addMovie({
tmdbId,
rootFolderPath: rootFolderPath.value,
monitor: monitor.value,
qualityProfileId: qualityProfileId.value,
minimumAvailability: minimumAvailability.value,
tags: tags.value,
searchForMovie
});
this.props.onModalClose(true);
}
//
// Render
render() {
return (
<AddNewMovieModalContent
{...this.props}
onInputChange={this.onInputChange}
onAddMoviePress={this.onAddMoviePress}
/>
);
}
}
AddNewDiscoverMovieModalContentConnector.propTypes = {
tmdbId: PropTypes.number.isRequired,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired,
onModalClose: PropTypes.func.isRequired,
setAddMovieDefault: PropTypes.func.isRequired,
addMovie: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewDiscoverMovieModalContentConnector);

View File

@@ -1,464 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageJumpBar from 'Components/Page/PageJumpBar';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props';
import styles from 'Movie/Index/MovieIndex.css';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import DiscoverMovieFooterConnector from './DiscoverMovieFooterConnector';
import DiscoverMovieFilterMenu from './Menus/DiscoverMovieFilterMenu';
import DiscoverMovieSortMenu from './Menus/DiscoverMovieSortMenu';
import DiscoverMovieViewMenu from './Menus/DiscoverMovieViewMenu';
import NoDiscoverMovie from './NoDiscoverMovie';
import DiscoverMovieOverviewsConnector from './Overview/DiscoverMovieOverviewsConnector';
import DiscoverMovieOverviewOptionsModal from './Overview/Options/DiscoverMovieOverviewOptionsModal';
import DiscoverMoviePostersConnector from './Posters/DiscoverMoviePostersConnector';
import DiscoverMoviePosterOptionsModal from './Posters/Options/DiscoverMoviePosterOptionsModal';
import DiscoverMovieTableConnector from './Table/DiscoverMovieTableConnector';
import DiscoverMovieTableOptionsConnector from './Table/DiscoverMovieTableOptionsConnector';
function getViewComponent(view) {
if (view === 'posters') {
return DiscoverMoviePostersConnector;
}
if (view === 'overview') {
return DiscoverMovieOverviewsConnector;
}
return DiscoverMovieTableConnector;
}
class DiscoverMovie extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
scroller: null,
jumpBarItems: { order: [] },
jumpToCharacter: null,
isPosterOptionsModalOpen: false,
isOverviewOptionsModalOpen: false,
isConfirmSearchModalOpen: false,
searchType: null,
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {}
};
}
componentDidMount() {
this.setJumpBarItems();
this.setSelectedState();
}
componentDidUpdate(prevProps) {
const {
items,
sortKey,
sortDirection
} = this.props;
if (sortKey !== prevProps.sortKey ||
sortDirection !== prevProps.sortDirection ||
hasDifferentItemsOrOrder(prevProps.items, items)
) {
this.setJumpBarItems();
this.setSelectedState();
}
if (this.state.jumpToCharacter != null) {
this.setState({ jumpToCharacter: null });
}
}
//
// Control
setScrollerRef = (ref) => {
this.setState({ scroller: ref });
}
getSelectedIds = () => {
if (this.state.allUnselected) {
return [];
}
return getSelectedIds(this.state.selectedState);
}
setSelectedState() {
const {
items
} = this.props;
const {
selectedState
} = this.state;
const newSelectedState = {};
items.forEach((movie) => {
const isItemSelected = selectedState[movie.tmdbId];
if (isItemSelected) {
newSelectedState[movie.tmdbId] = isItemSelected;
} else {
newSelectedState[movie.tmdbId] = false;
}
});
const selectedCount = getSelectedIds(newSelectedState).length;
const newStateCount = Object.keys(newSelectedState).length;
let isAllSelected = false;
let isAllUnselected = false;
if (selectedCount === 0) {
isAllUnselected = true;
} else if (selectedCount === newStateCount) {
isAllSelected = true;
}
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
}
setJumpBarItems() {
const {
items,
sortKey,
sortDirection
} = this.props;
// Reset if not sorting by sortTitle
if (sortKey !== 'sortTitle') {
this.setState({ jumpBarItems: { order: [] } });
return;
}
const characters = _.reduce(items, (acc, item) => {
let char = item.sortTitle.charAt(0);
if (!isNaN(char)) {
char = '#';
}
if (char in acc) {
acc[char] = acc[char] + 1;
} else {
acc[char] = 1;
}
return acc;
}, {});
const order = Object.keys(characters).sort();
// Reverse if sorting descending
if (sortDirection === sortDirections.DESCENDING) {
order.reverse();
}
const jumpBarItems = {
characters,
order
};
this.setState({ jumpBarItems });
}
//
// Listeners
onPosterOptionsPress = () => {
this.setState({ isPosterOptionsModalOpen: true });
}
onPosterOptionsModalClose = () => {
this.setState({ isPosterOptionsModalOpen: false });
}
onOverviewOptionsPress = () => {
this.setState({ isOverviewOptionsModalOpen: true });
}
onOverviewOptionsModalClose = () => {
this.setState({ isOverviewOptionsModalOpen: false });
}
onJumpBarItemPress = (jumpToCharacter) => {
this.setState({ jumpToCharacter });
}
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectAllPress = () => {
this.onSelectAllChange({ value: !this.state.allSelected });
}
onImportListSyncPress = () => {
this.props.onImportListSyncPress();
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey, 'tmdbId');
});
}
onAddMoviesPress = ({ addOptions }) => {
this.props.onAddMoviesPress({ ids: this.getSelectedIds(), addOptions });
}
onExcludeMoviesPress = () => {
this.props.onExcludeMoviesPress({ ids: this.getSelectedIds() });
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
totalItems,
items,
columns,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
view,
onSortSelect,
onFilterSelect,
onViewSelect,
onScroll,
onAddMoviesPress,
isSyncingLists,
...otherProps
} = this.props;
const {
scroller,
jumpBarItems,
jumpToCharacter,
isPosterOptionsModalOpen,
isOverviewOptionsModalOpen,
selectedState,
allSelected,
allUnselected
} = this.state;
const selectedMovieIds = this.getSelectedIds();
const ViewComponent = getViewComponent(view);
const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoMovie = !totalItems;
return (
<PageContent>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label='Refresh Lists'
iconName={icons.REFRESH}
isSpinning={isSyncingLists}
isDisabled={hasNoMovie}
onPress={this.onImportListSyncPress}
/>
<PageToolbarButton
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
iconName={icons.CHECK_SQUARE}
isDisabled={hasNoMovie}
onPress={this.onSelectAllPress}
/>
</PageToolbarSection>
<PageToolbarSection
alignContent={align.RIGHT}
collapseButtons={false}
>
{
view === 'table' ?
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
optionsComponent={DiscoverMovieTableOptionsConnector}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper> :
null
}
{
view === 'posters' ?
<PageToolbarButton
label={translate('Options')}
iconName={icons.POSTER}
onPress={this.onPosterOptionsPress}
/> :
null
}
{
view === 'overview' ?
<PageToolbarButton
label={translate('Options')}
iconName={icons.OVERVIEW}
onPress={this.onOverviewOptionsPress}
/> :
null
}
{
(view === 'posters' || view === 'overview') &&
<PageToolbarSeparator />
}
<DiscoverMovieViewMenu
view={view}
isDisabled={hasNoMovie}
onViewSelect={onViewSelect}
/>
<DiscoverMovieSortMenu
sortKey={sortKey}
sortDirection={sortDirection}
isDisabled={hasNoMovie}
onSortSelect={onSortSelect}
/>
<DiscoverMovieFilterMenu
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
isDisabled={hasNoMovie}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<div className={styles.pageContentBodyWrapper}>
<PageContentBody
registerScroller={this.setScrollerRef}
className={styles.contentBody}
innerClassName={styles[`${view}InnerContentBody`]}
onScroll={onScroll}
>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>
{translate('UnableToLoadMovies')}
</div>
}
{
isLoaded &&
<div className={styles.contentBodyContainer}>
<ViewComponent
scroller={scroller}
items={items}
filters={filters}
sortKey={sortKey}
sortDirection={sortDirection}
jumpToCharacter={jumpToCharacter}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectedChange={this.onSelectedChange}
onSelectAllChange={this.onSelectAllChange}
selectedState={selectedState}
{...otherProps}
/>
</div>
}
{
!error && isPopulated && !items.length &&
<NoDiscoverMovie totalItems={totalItems} />
}
</PageContentBody>
{
isLoaded && !!jumpBarItems.order.length &&
<PageJumpBar
items={jumpBarItems}
onItemPress={this.onJumpBarItemPress}
/>
}
</div>
{
isLoaded &&
<DiscoverMovieFooterConnector
selectedIds={selectedMovieIds}
onAddMoviesPress={this.onAddMoviesPress}
onExcludeMoviesPress={this.onExcludeMoviesPress}
/>
}
<DiscoverMoviePosterOptionsModal
isOpen={isPosterOptionsModalOpen}
onModalClose={this.onPosterOptionsModalClose}
/>
<DiscoverMovieOverviewOptionsModal
isOpen={isOverviewOptionsModalOpen}
onModalClose={this.onOverviewOptionsModalClose}
/>
</PageContent>
);
}
}
DiscoverMovie.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
totalItems: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
view: PropTypes.string.isRequired,
isSyncingLists: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onViewSelect: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired,
onAddMoviesPress: PropTypes.func.isRequired,
onExcludeMoviesPress: PropTypes.func.isRequired,
onImportListSyncPress: PropTypes.func.isRequired
};
export default DiscoverMovie;

View File

@@ -1,155 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition';
import { executeCommand } from 'Store/Actions/commandActions';
import { addImportExclusions, addMovies, clearAddMovie, fetchDiscoverMovies, setListMovieFilter, setListMovieSort, setListMovieTableOption, setListMovieView } from 'Store/Actions/discoverMovieActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions';
import scrollPositions from 'Store/scrollPositions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createDiscoverMovieClientSideCollectionItemsSelector from 'Store/Selectors/createDiscoverMovieClientSideCollectionItemsSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import DiscoverMovie from './DiscoverMovie';
function createMapStateToProps() {
return createSelector(
createDiscoverMovieClientSideCollectionItemsSelector('discoverMovie'),
createCommandExecutingSelector(commandNames.IMPORT_LIST_SYNC),
createDimensionsSelector(),
(
movies,
isSyncingLists,
dimensionsState
) => {
return {
...movies,
isSyncingLists,
isSmallScreen: dimensionsState.isSmallScreen
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
dispatchFetchRootFolders() {
dispatch(fetchRootFolders());
},
dispatchFetchImportExclusions() {
dispatch(fetchImportExclusions());
},
dispatchClearListMovie() {
dispatch(clearAddMovie());
},
dispatchFetchListMovies() {
dispatch(fetchDiscoverMovies());
},
onTableOptionChange(payload) {
dispatch(setListMovieTableOption(payload));
},
onSortSelect(sortKey) {
dispatch(setListMovieSort({ sortKey }));
},
onFilterSelect(selectedFilterKey) {
dispatch(setListMovieFilter({ selectedFilterKey }));
},
dispatchSetListMovieView(view) {
dispatch(setListMovieView({ view }));
},
dispatchAddMovies(ids, addOptions) {
dispatch(addMovies({ ids, addOptions }));
},
dispatchAddImportExclusions(exclusions) {
dispatch(addImportExclusions(exclusions));
},
onImportListSyncPress() {
dispatch(executeCommand({
name: commandNames.IMPORT_LIST_SYNC
}));
}
};
}
class DiscoverMovieConnector extends Component {
//
// Lifecycle
componentDidMount() {
registerPagePopulator(this.repopulate);
this.props.dispatchFetchRootFolders();
this.props.dispatchFetchImportExclusions();
this.props.dispatchFetchListMovies();
}
componentWillUnmount() {
this.props.dispatchClearListMovie();
unregisterPagePopulator(this.repopulate);
}
//
// Listeners
onViewSelect = (view) => {
this.props.dispatchSetListMovieView(view);
}
onScroll = ({ scrollTop }) => {
scrollPositions.discoverMovie = scrollTop;
}
onAddMoviesPress = ({ ids, addOptions }) => {
this.props.dispatchAddMovies(ids, addOptions);
}
onExcludeMoviesPress =({ ids }) => {
this.props.dispatchAddImportExclusions({ ids });
}
//
// Render
render() {
return (
<DiscoverMovie
{...this.props}
onViewSelect={this.onViewSelect}
onScroll={this.onScroll}
onAddMoviesPress={this.onAddMoviesPress}
onExcludeMoviesPress={this.onExcludeMoviesPress}
onSyncListsPress={this.onSyncListsPress}
/>
);
}
}
DiscoverMovieConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired,
dispatchFetchImportExclusions: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchFetchListMovies: PropTypes.func.isRequired,
dispatchClearListMovie: PropTypes.func.isRequired,
dispatchSetListMovieView: PropTypes.func.isRequired,
dispatchAddMovies: PropTypes.func.isRequired,
dispatchAddImportExclusions: PropTypes.func.isRequired
};
export default withScrollPosition(
connect(createMapStateToProps, createMapDispatchToProps)(DiscoverMovieConnector),
'discoverMovie'
);

View File

@@ -1,24 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterModal from 'Components/Filter/FilterModal';
import { setListMovieFilter } from 'Store/Actions/discoverMovieActions';
function createMapStateToProps() {
return createSelector(
(state) => state.discoverMovie.items,
(state) => state.discoverMovie.filterBuilderProps,
(sectionItems, filterBuilderProps) => {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'discoverMovie'
};
}
);
}
const mapDispatchToProps = {
dispatchSetFilter: setListMovieFilter
};
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);

View File

@@ -1,56 +0,0 @@
.inputContainer {
margin-right: 20px;
min-width: 150px;
}
.buttonContainer {
display: flex;
justify-content: flex-end;
flex-grow: 1;
}
.buttonContainerContent {
flex-grow: 0;
}
.buttons {
display: flex;
justify-content: flex-end;
flex-grow: 1;
}
.addSelectedButton {
composes: button from '~Components/Link/SpinnerButton.css';
margin-right: 10px;
height: 35px;
}
.excludeSelectedButton {
composes: button from '~Components/Link/SpinnerButton.css';
margin-left: 25px;
height: 35px;
}
@media only screen and (max-width: $breakpointSmall) {
.inputContainer {
margin-right: 0;
}
.buttonContainer {
justify-content: flex-start;
}
.buttonContainerContent {
flex-grow: 1;
}
.buttons {
justify-content: space-between;
}
.selectedMovieLabel {
text-align: left;
}
}

View File

@@ -1,277 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AvailabilitySelectInput from 'Components/Form/AvailabilitySelectInput';
import CheckInput from 'Components/Form/CheckInput';
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import DiscoverMovieFooterLabel from './DiscoverMovieFooterLabel';
import ExcludeMovieModal from './Exclusion/ExcludeMovieModal';
import styles from './DiscoverMovieFooter.css';
class DiscoverMovieFooter extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
defaultMonitor,
defaultQualityProfileId,
defaultMinimumAvailability,
defaultRootFolderPath,
defaultSearchForMovie
} = props;
this.state = {
monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId,
minimumAvailability: defaultMinimumAvailability,
rootFolderPath: defaultRootFolderPath,
searchForMovie: defaultSearchForMovie,
isExcludeMovieModalOpen: false,
destinationRootFolder: null
};
}
componentDidUpdate(prevProps) {
const {
defaultMonitor,
defaultQualityProfileId,
defaultMinimumAvailability,
defaultRootFolderPath,
defaultSearchForMovie
} = this.props;
const {
monitor,
qualityProfileId,
minimumAvailability,
rootFolderPath,
searchForMovie
} = this.state;
const newState = {};
if (monitor !== defaultMonitor) {
newState.monitor = defaultMonitor;
}
if (qualityProfileId !== defaultQualityProfileId) {
newState.qualityProfileId = defaultQualityProfileId;
}
if (minimumAvailability !== defaultMinimumAvailability) {
newState.minimumAvailability = defaultMinimumAvailability;
}
if (rootFolderPath !== defaultRootFolderPath) {
newState.rootFolderPath = defaultRootFolderPath;
}
if (searchForMovie !== defaultSearchForMovie) {
newState.searchForMovie = defaultSearchForMovie;
}
if (!_.isEmpty(newState)) {
this.setState(newState);
}
}
//
// Listeners
onExcludeSelectedPress = () => {
this.setState({ isExcludeMovieModalOpen: true });
}
onExcludeMovieModalClose = () => {
this.setState({ isExcludeMovieModalOpen: false });
}
onAddMoviesPress = () => {
const {
monitor,
qualityProfileId,
minimumAvailability,
rootFolderPath,
searchForMovie
} = this.state;
const addOptions = {
monitor,
qualityProfileId,
minimumAvailability,
rootFolderPath,
searchForMovie
};
this.props.onAddMoviesPress({ addOptions });
}
//
// Render
render() {
const {
selectedIds,
selectedCount,
isAdding,
isExcluding,
onInputChange
} = this.props;
const {
monitor,
qualityProfileId,
minimumAvailability,
rootFolderPath,
searchForMovie,
isExcludeMovieModalOpen
} = this.state;
const monitoredOptions = [
{ key: true, value: 'Monitored' },
{ key: false, value: 'Unmonitored' }
];
return (
<PageContentFooter>
<div className={styles.inputContainer}>
<DiscoverMovieFooterLabel
label={translate('MonitorMovie')}
isSaving={isAdding}
/>
<SelectInput
name="monitor"
value={monitor}
values={monitoredOptions}
isDisabled={!selectedCount}
onChange={onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<DiscoverMovieFooterLabel
label={translate('QualityProfile')}
isSaving={isAdding}
/>
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
isDisabled={!selectedCount}
onChange={onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<DiscoverMovieFooterLabel
label={translate('MinimumAvailability')}
isSaving={isAdding}
/>
<AvailabilitySelectInput
name="minimumAvailability"
value={minimumAvailability}
isDisabled={!selectedCount}
onChange={onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<DiscoverMovieFooterLabel
label={translate('RootFolder')}
isSaving={isAdding}
/>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<DiscoverMovieFooterLabel
label={translate('SearchOnAdd')}
isSaving={isAdding}
/>
<CheckInput
name="searchForMovie"
isDisabled={!selectedCount}
value={searchForMovie}
onChange={onInputChange}
/>
</div>
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<DiscoverMovieFooterLabel
label={translate('MoviesSelectedInterp', [selectedCount])}
isSaving={false}
/>
<div className={styles.buttons}>
<div>
<SpinnerButton
className={styles.addSelectedButton}
kind={kinds.PRIMARY}
isSpinning={isAdding}
isDisabled={!selectedCount || isAdding}
onPress={this.onAddMoviesPress}
>
{translate('AddMovies')}
</SpinnerButton>
<SpinnerButton
className={styles.excludeSelectedButton}
kind={kinds.DANGER}
isSpinning={isExcluding}
isDisabled={!selectedCount || isExcluding}
onPress={this.props.onExcludeMoviesPress}
>
{translate('AddExclusion')}
</SpinnerButton>
</div>
</div>
</div>
</div>
<ExcludeMovieModal
isOpen={isExcludeMovieModalOpen}
movieIds={selectedIds}
onModalClose={this.onExcludeMovieModalClose}
/>
</PageContentFooter>
);
}
}
DiscoverMovieFooter.propTypes = {
selectedIds: PropTypes.arrayOf(PropTypes.number).isRequired,
selectedCount: PropTypes.number.isRequired,
isAdding: PropTypes.bool.isRequired,
isExcluding: PropTypes.bool.isRequired,
defaultMonitor: PropTypes.string.isRequired,
defaultQualityProfileId: PropTypes.number,
defaultMinimumAvailability: PropTypes.string,
defaultRootFolderPath: PropTypes.string,
defaultSearchForMovie: PropTypes.bool,
onInputChange: PropTypes.func.isRequired,
onAddMoviesPress: PropTypes.func.isRequired,
onExcludeMoviesPress: PropTypes.func.isRequired
};
export default DiscoverMovieFooter;

View File

@@ -1,74 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setAddMovieDefault } from 'Store/Actions/discoverMovieActions';
import DiscoverMovieFooter from './DiscoverMovieFooter';
function createMapStateToProps() {
return createSelector(
(state) => state.discoverMovie,
(state) => state.settings.importExclusions,
(state, { selectedIds }) => selectedIds,
(discoverMovie, importExclusions, selectedIds) => {
const {
monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId,
minimumAvailability: defaultMinimumAvailability,
rootFolderPath: defaultRootFolderPath,
searchForMovie: defaultSearchForMovie
} = discoverMovie.defaults;
const {
isAdding
} = discoverMovie;
const {
isSaving
} = importExclusions;
return {
selectedCount: selectedIds.length,
isAdding,
isExcluding: isSaving,
defaultMonitor,
defaultQualityProfileId,
defaultMinimumAvailability,
defaultRootFolderPath,
defaultSearchForMovie
};
}
);
}
const mapDispatchToProps = {
setAddMovieDefault
};
class DiscoverMovieFooterConnector extends Component {
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setAddMovieDefault({ [name]: value });
}
//
// Render
render() {
return (
<DiscoverMovieFooter
{...this.props}
onInputChange={this.onInputChange}
/>
);
}
}
DiscoverMovieFooterConnector.propTypes = {
setAddMovieDefault: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(DiscoverMovieFooterConnector);

View File

@@ -1,8 +0,0 @@
.label {
margin-bottom: 3px;
font-weight: bold;
}
.savingIcon {
margin-left: 8px;
}

View File

@@ -1,40 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import SpinnerIcon from 'Components/SpinnerIcon';
import { icons } from 'Helpers/Props';
import styles from './DiscoverMovieFooterLabel.css';
function DiscoverMovieFooterLabel(props) {
const {
className,
label,
isSaving
} = props;
return (
<div className={className}>
{label}
{
isSaving &&
<SpinnerIcon
className={styles.savingIcon}
name={icons.SPINNER}
isSpinning={true}
/>
}
</div>
);
}
DiscoverMovieFooterLabel.propTypes = {
className: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired
};
DiscoverMovieFooterLabel.defaultProps = {
className: styles.label
};
export default DiscoverMovieFooterLabel;

View File

@@ -1,60 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createDiscoverMovieSelector from 'Store/Selectors/createDiscoverMovieSelector';
function createMapStateToProps() {
return createSelector(
createDiscoverMovieSelector(),
(
movie
) => {
// If a movie is deleted this selector may fire before the parent
// selecors, which will result in an undefined movie, if that happens
// we want to return early here and again in the render function to avoid
// trying to show a movie that has no information available.
if (!movie) {
return {};
}
return {
...movie
};
}
);
}
class DiscoverMovieItemConnector extends Component {
//
// Render
render() {
const {
tmdbId,
component: ItemComponent,
...otherProps
} = this.props;
if (!tmdbId) {
return null;
}
return (
<ItemComponent
{...otherProps}
tmdbId={tmdbId}
/>
);
}
}
DiscoverMovieItemConnector.propTypes = {
tmdbId: PropTypes.number,
component: PropTypes.elementType.isRequired
};
export default connect(createMapStateToProps)(DiscoverMovieItemConnector);

View File

@@ -1,33 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import ExcludeMovieModalContentConnector from './ExcludeMovieModalContentConnector';
function ExcludeMovieModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={onModalClose}
>
<ExcludeMovieModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
ExcludeMovieModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default ExcludeMovieModal;

View File

@@ -1,12 +0,0 @@
.pathContainer {
margin-bottom: 20px;
}
.pathIcon {
margin-right: 8px;
}
.deleteFilesMessage {
margin-top: 20px;
color: $dangerColor;
}

View File

@@ -1,70 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ExcludeMovieModalContent.css';
class ExcludeMovieModalContent extends Component {
//
// Listeners
onExcludeMovieConfirmed = () => {
this.props.onExcludePress();
}
//
// Render
render() {
const {
tmdbId,
title,
onModalClose
} = this.props;
return (
<ModalContent
onModalClose={onModalClose}
>
<ModalHeader>
Exclude - {title} ({tmdbId})
</ModalHeader>
<ModalBody>
<div className={styles.pathContainer}>
Exclude {title}? This will prevent Radarr from adding automatically via list sync.
</div>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onExcludeMovieConfirmed}
>
Exclude
</Button>
</ModalFooter>
</ModalContent>
);
}
}
ExcludeMovieModalContent.propTypes = {
tmdbId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
onExcludePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default ExcludeMovieModalContent;

View File

@@ -1,43 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addImportExclusions } from 'Store/Actions/discoverMovieActions';
import ExcludeMovieModalContent from './ExcludeMovieModalContent';
const mapDispatchToProps = {
addImportExclusions
};
class ExcludeMovieModalContentConnector extends Component {
//
// Listeners
onExcludePress = () => {
this.props.addImportExclusions({ ids: [this.props.tmdbId] });
this.props.onModalClose(true);
}
//
// Render
render() {
return (
<ExcludeMovieModalContent
{...this.props}
onExcludePress={this.onExcludePress}
/>
);
}
}
ExcludeMovieModalContentConnector.propTypes = {
tmdbId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired,
addImportExclusions: PropTypes.func.isRequired
};
export default connect(undefined, mapDispatchToProps)(ExcludeMovieModalContentConnector);

View File

@@ -1,41 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import FilterMenu from 'Components/Menu/FilterMenu';
import DiscoverMovieFilterModalConnector from 'DiscoverMovie/DiscoverMovieFilterModalConnector';
import { align } from 'Helpers/Props';
function DiscoverMovieFilterMenu(props) {
const {
selectedFilterKey,
filters,
customFilters,
isDisabled,
onFilterSelect
} = props;
return (
<FilterMenu
alignMenu={align.RIGHT}
isDisabled={isDisabled}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={DiscoverMovieFilterModalConnector}
onFilterSelect={onFilterSelect}
/>
);
}
DiscoverMovieFilterMenu.propTypes = {
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
onFilterSelect: PropTypes.func.isRequired
};
DiscoverMovieFilterMenu.defaultProps = {
showCustomFilters: false
};
export default DiscoverMovieFilterMenu;

View File

@@ -1,114 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import MenuContent from 'Components/Menu/MenuContent';
import SortMenu from 'Components/Menu/SortMenu';
import SortMenuItem from 'Components/Menu/SortMenuItem';
import { align, sortDirections } from 'Helpers/Props';
function DiscoverMovieSortMenu(props) {
const {
sortKey,
sortDirection,
isDisabled,
onSortSelect
} = props;
return (
<SortMenu
isDisabled={isDisabled}
alignMenu={align.RIGHT}
>
<MenuContent>
<SortMenuItem
name="status"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
Status
</SortMenuItem>
<SortMenuItem
name="sortTitle"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
Title
</SortMenuItem>
<SortMenuItem
name="studio"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
Studio
</SortMenuItem>
<SortMenuItem
name="inCinemas"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
In Cinemas
</SortMenuItem>
<SortMenuItem
name="physicalRelease"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
Physical Release
</SortMenuItem>
<SortMenuItem
name="digitalRelease"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
Digital Release
</SortMenuItem>
<SortMenuItem
name="runtime"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
Runtime
</SortMenuItem>
<SortMenuItem
name="ratings"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
Rating
</SortMenuItem>
<SortMenuItem
name="certification"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
Certification
</SortMenuItem>
</MenuContent>
</SortMenu>
);
}
DiscoverMovieSortMenu.propTypes = {
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
isDisabled: PropTypes.bool.isRequired,
onSortSelect: PropTypes.func.isRequired
};
export default DiscoverMovieSortMenu;

View File

@@ -1,55 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import MenuContent from 'Components/Menu/MenuContent';
import ViewMenu from 'Components/Menu/ViewMenu';
import ViewMenuItem from 'Components/Menu/ViewMenuItem';
import { align } from 'Helpers/Props';
function DiscoverMovieViewMenu(props) {
const {
view,
isDisabled,
onViewSelect
} = props;
return (
<ViewMenu
isDisabled={isDisabled}
alignMenu={align.RIGHT}
>
<MenuContent>
<ViewMenuItem
name="table"
selectedView={view}
onPress={onViewSelect}
>
Table
</ViewMenuItem>
<ViewMenuItem
name="posters"
selectedView={view}
onPress={onViewSelect}
>
Posters
</ViewMenuItem>
<ViewMenuItem
name="overview"
selectedView={view}
onPress={onViewSelect}
>
Overview
</ViewMenuItem>
</MenuContent>
</ViewMenu>
);
}
DiscoverMovieViewMenu.propTypes = {
view: PropTypes.string.isRequired,
isDisabled: PropTypes.bool.isRequired,
onViewSelect: PropTypes.func.isRequired
};
export default DiscoverMovieViewMenu;

View File

@@ -1,11 +0,0 @@
.message {
margin-top: 10px;
margin-bottom: 30px;
text-align: center;
font-size: 20px;
}
.buttonContainer {
margin-top: 20px;
text-align: center;
}

View File

@@ -1,61 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './NoDiscoverMovie.css';
function NoDiscoverMovie(props) {
const { totalItems } = props;
if (totalItems > 0) {
return (
<div>
<div className={styles.message}>
{translate('AllMoviesHiddenDueToFilter')}
</div>
</div>
);
}
return (
<div>
<div className={styles.message}>
No list items or recommendations found, to get started you'll want to add a new movie, import some existing ones, or add a list.
</div>
<div className={styles.buttonContainer}>
<Button
to="/add/import"
kind={kinds.PRIMARY}
>
{translate('ImportExistingMovies')}
</Button>
</div>
<div className={styles.buttonContainer}>
<Button
to="/add/new"
kind={kinds.PRIMARY}
>
{translate('AddNewMovie')}
</Button>
</div>
<div className={styles.buttonContainer}>
<Button
to="/settings/importlists"
kind={kinds.PRIMARY}
>
{translate('AddList')}
</Button>
</div>
</div>
);
}
NoDiscoverMovie.propTypes = {
totalItems: PropTypes.number.isRequired
};
export default NoDiscoverMovie;

View File

@@ -1,100 +0,0 @@
$hoverScale: 1.05;
.content {
display: flex;
flex-grow: 1;
}
.poster {
position: relative;
}
.editorSelect {
position: absolute;
top: 0;
left: 5px;
z-index: 3;
}
.posterContainer {
position: relative;
}
.link {
composes: link from '~Components/Link/Link.css';
display: block;
color: $defaultColor;
&:hover {
color: $defaultColor;
text-decoration: none;
}
}
.alreadyExistsIcon {
margin-left: 10px;
color: #37bc9b;
pointer-events: all;
}
.exclusionIcon {
margin-left: 10px;
color: $dangerColor;
pointer-events: all;
}
.info {
display: flex;
flex: 1 0 1px;
flex-direction: column;
overflow: hidden;
padding-left: 10px;
}
.titleRow {
display: flex;
justify-content: space-between;
flex: 0 0 auto;
margin-bottom: 5px;
line-height: 32px;
}
.lists {
display: flex;
margin-bottom: 5px;
}
.title {
@add-mixin truncate;
composes: link;
flex: 1 0 1px;
font-weight: 300;
font-size: 30px;
line-height: 1;
}
.actions {
white-space: nowrap;
}
.details {
display: flex;
justify-content: space-between;
flex: 1 0 auto;
}
.overview {
composes: link;
flex: 0 1 1000px;
overflow: hidden;
min-height: 0;
}
@media only screen and (max-width: $breakpointSmall) {
.overview {
display: none;
}
}

View File

@@ -1,294 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import CheckInput from 'Components/Form/CheckInput';
import Icon from 'Components/Icon';
import ImportListListConnector from 'Components/ImportListListConnector';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import Popover from 'Components/Tooltip/Popover';
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
import { icons, kinds } from 'Helpers/Props';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import MoviePoster from 'Movie/MoviePoster';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import translate from 'Utilities/String/translate';
import DiscoverMovieOverviewInfo from './DiscoverMovieOverviewInfo';
import styles from './DiscoverMovieOverview.css';
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
// Hardcoded height beased on line-height of 32 + bottom margin of 10. 19 + 5 for List Row
// Less side-effecty than using react-measure.
const titleRowHeight = 66;
function getContentHeight(rowHeight, isSmallScreen) {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
return rowHeight - padding;
}
class DiscoverMovieOverview extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isNewAddMovieModalOpen: false,
isExcludeMovieModalOpen: false
};
}
//
// Listeners
onPress = () => {
this.setState({ isNewAddMovieModalOpen: true });
}
onAddMovieModalClose = () => {
this.setState({ isNewAddMovieModalOpen: false });
}
onExcludeMoviePress = () => {
this.setState({ isExcludeMovieModalOpen: true });
}
onExcludeMovieModalClose = () => {
this.setState({ isExcludeMovieModalOpen: false });
}
onChange = ({ value, shiftKey }) => {
const {
tmdbId,
onSelectedChange
} = this.props;
onSelectedChange({ id: tmdbId, value, shiftKey });
}
//
// Render
render() {
const {
tmdbId,
imdbId,
youTubeTrailerId,
title,
folder,
year,
overview,
images,
lists,
posterWidth,
posterHeight,
rowHeight,
isSmallScreen,
isExisting,
isExcluded,
isRecommendation,
isSelected,
overviewOptions,
...otherProps
} = this.props;
const {
isNewAddMovieModalOpen,
isExcludeMovieModalOpen
} = this.state;
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
};
const linkProps = isExisting ? { to: `/movie/${tmdbId}` } : { onPress: this.onPress };
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
const overviewHeight = contentHeight - titleRowHeight;
return (
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.poster}>
<div className={styles.posterContainer}>
<div className={styles.editorSelect}>
<CheckInput
className={styles.checkInput}
name={tmdbId.toString()}
value={isSelected}
onChange={this.onChange}
/>
</div>
<MoviePoster
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
/>
</div>
</div>
<div className={styles.info} style={{ maxHeight: contentHeight }}>
<div className={styles.titleRow}>
<Link
className={styles.title}
{...linkProps}
>
{title}
{
isExisting ?
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={30}
title={translate('AlreadyInYourLibrary')}
/> : null
}
{
isExcluded &&
<Icon
className={styles.exclusionIcon}
name={icons.DANGER}
size={30}
title='Movie is on Import Exclusion List'
/>
}
</Link>
<div className={styles.actions}>
<span className={styles.externalLinks}>
<Popover
anchor={
<Icon
name={icons.EXTERNAL_LINK}
size={12}
/>
}
title={translate('Links')}
body={
<MovieDetailsLinks
tmdbId={tmdbId}
imdbId={imdbId}
youTubeTrailerId={youTubeTrailerId}
/>
}
/>
</span>
<IconButton
name={icons.REMOVE}
title={isExcluded ? translate('MovieAlreadyExcluded') : translate('ExcludeMovie')}
onPress={this.onExcludeMoviePress}
isDisabled={isExcluded}
/>
</div>
</div>
<div className={styles.lists}>
{
isRecommendation ?
<Label
kind={kinds.INFO}
>
<Icon
name={icons.RECOMMENDED}
size={10}
/>
</Label> :
null
}
<ImportListListConnector
lists={lists}
/>
</div>
<div className={styles.details}>
<div className={styles.overview}>
<TextTruncate
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
text={overview}
/>
</div>
<DiscoverMovieOverviewInfo
height={overviewHeight}
year={year}
{...overviewOptions}
{...otherProps}
/>
</div>
</div>
</div>
<AddNewDiscoverMovieModal
isOpen={isNewAddMovieModalOpen && !isExisting}
tmdbId={tmdbId}
title={title}
year={year}
overview={overview}
folder={folder}
images={images}
onModalClose={this.onAddMovieModalClose}
/>
<ExcludeMovieModal
isOpen={isExcludeMovieModalOpen}
tmdbId={tmdbId}
title={title}
year={year}
onModalClose={this.onExcludeMovieModalClose}
/>
</div>
);
}
}
DiscoverMovieOverview.propTypes = {
tmdbId: PropTypes.number.isRequired,
imdbId: PropTypes.string,
youTubeTrailerId: PropTypes.string,
title: PropTypes.string.isRequired,
folder: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
overview: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,
rowHeight: PropTypes.number.isRequired,
overviewOptions: PropTypes.object.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
isExisting: PropTypes.bool.isRequired,
isExcluded: PropTypes.bool.isRequired,
isRecommendation: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
lists: PropTypes.arrayOf(PropTypes.number).isRequired,
onSelectedChange: PropTypes.func.isRequired
};
DiscoverMovieOverview.defaultProps = {
lists: []
};
export default DiscoverMovieOverview;

View File

@@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import DiscoverMovieOverview from './DiscoverMovieOverview';
function createMapStateToProps() {
return createSelector(
createDimensionsSelector(),
(dimensions) => {
return {
isSmallScreen: dimensions.isSmallScreen
};
}
);
}
export default connect(createMapStateToProps)(DiscoverMovieOverview);

View File

@@ -1,12 +0,0 @@
.infos {
display: flex;
flex: 0 0 250px;
flex-direction: column;
margin-left: 10px;
}
@media only screen and (max-width: $breakpointSmall) {
.infos {
margin-left: 0;
}
}

View File

@@ -1,147 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import DiscoverMovieOverviewInfoRow from './DiscoverMovieOverviewInfoRow';
import styles from './DiscoverMovieOverviewInfo.css';
const infoRowHeight = parseInt(dimensions.movieIndexOverviewInfoRowHeight);
const rows = [
{
name: 'year',
showProp: 'showYear',
valueProp: 'year'
},
{
name: 'genres',
showProp: 'showGenres',
valueProp: 'genres'
},
{
name: 'ratings',
showProp: 'showRatings',
valueProp: 'ratings'
},
{
name: 'certification',
showProp: 'showCertification',
valueProp: 'certification'
},
{
name: 'studio',
showProp: 'showStudio',
valueProp: 'studio'
}
];
function isVisible(row, props) {
const {
name,
showProp,
valueProp
} = row;
if (props[valueProp] == null) {
return false;
}
return props[showProp] || props.sortKey === name;
}
function getInfoRowProps(row, props) {
const { name } = row;
if (name === 'year') {
return {
title: 'Year',
iconName: icons.CALENDAR,
label: props.year
};
}
if (name === 'genres') {
return {
title: 'Genres',
iconName: icons.GENRE,
label: props.genres.slice(0, 2).join(', ')
};
}
if (name === 'ratings') {
return {
title: 'Ratings',
iconName: icons.HEART,
label: `${props.ratings.value * 10}%`
};
}
if (name === 'certification') {
return {
title: 'Certification',
iconName: icons.FILM,
label: props.certification
};
}
if (name === 'studio') {
return {
title: 'Studio',
iconName: icons.STUDIO,
label: props.studio
};
}
}
function DiscoverMovieOverviewInfo(props) {
const {
height
} = props;
let shownRows = 1;
const maxRows = Math.floor(height / (infoRowHeight + 4));
return (
<div className={styles.infos}>
{
rows.map((row) => {
if (!isVisible(row, props)) {
return null;
}
if (shownRows >= maxRows) {
return null;
}
shownRows++;
const infoRowProps = getInfoRowProps(row, props);
return (
<DiscoverMovieOverviewInfoRow
key={row.name}
{...infoRowProps}
/>
);
})
}
</div>
);
}
DiscoverMovieOverviewInfo.propTypes = {
height: PropTypes.number.isRequired,
showStudio: PropTypes.bool.isRequired,
showYear: PropTypes.bool.isRequired,
showRatings: PropTypes.bool.isRequired,
showCertification: PropTypes.bool.isRequired,
showGenres: PropTypes.bool.isRequired,
studio: PropTypes.string,
year: PropTypes.number,
certification: PropTypes.string,
ratings: PropTypes.object,
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
sortKey: PropTypes.string.isRequired
};
export default DiscoverMovieOverviewInfo;

View File

@@ -1,10 +0,0 @@
.infoRow {
flex: 0 0 $movieIndexOverviewInfoRowHeight;
margin: 2px 0;
}
.icon {
margin-right: 5px;
width: 25px !important;
text-align: center;
}

View File

@@ -1,35 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import styles from './DiscoverMovieOverviewInfoRow.css';
function DiscoverMovieOverviewInfoRow(props) {
const {
title,
iconName,
label
} = props;
return (
<div
className={styles.infoRow}
title={title}
>
<Icon
className={styles.icon}
name={iconName}
size={14}
/>
{label}
</div>
);
}
DiscoverMovieOverviewInfoRow.propTypes = {
title: PropTypes.string,
iconName: PropTypes.object.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired
};
export default DiscoverMovieOverviewInfoRow;

View File

@@ -1,15 +0,0 @@
.grid {
flex: 1 0 auto;
}
.container {
&:hover {
.content {
background-color: $tableRowHoverBackgroundColor;
}
}
}
.externalLinks {
margin-right: 0.5em;
}

View File

@@ -1,261 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Grid, WindowScroller } from 'react-virtualized';
import Measure from 'Components/Measure';
import DiscoverMovieItemConnector from 'DiscoverMovie/DiscoverMovieItemConnector';
import dimensions from 'Styles/Variables/dimensions';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import DiscoverMovieOverviewConnector from './DiscoverMovieOverviewConnector';
import styles from './DiscoverMovieOverviews.css';
// Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
function calculatePosterWidth(posterSize, isSmallScreen) {
const maxiumPosterWidth = isSmallScreen ? 152 : 162;
if (posterSize === 'large') {
return maxiumPosterWidth;
}
if (posterSize === 'medium') {
return Math.floor(maxiumPosterWidth * 0.75);
}
return Math.floor(maxiumPosterWidth * 0.5);
}
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
const heights = [
posterHeight,
isSmallScreen ? columnPaddingSmallScreen : columnPadding
];
return heights.reduce((acc, height) => acc + height, 0);
}
function calculatePosterHeight(posterWidth) {
return Math.ceil((250 / 170) * posterWidth);
}
class DiscoverMovieOverviews extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
width: 0,
columnCount: 1,
posterWidth: 162,
posterHeight: 238,
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {})
};
this._grid = null;
}
componentDidUpdate(prevProps, prevState) {
const {
items,
sortKey,
overviewOptions,
jumpToCharacter
} = this.props;
const {
width,
rowHeight
} = this.state;
if (prevProps.sortKey !== sortKey ||
prevProps.overviewOptions !== overviewOptions) {
this.calculateGrid();
}
if (this._grid &&
(prevState.width !== width ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items, 'tmdbId'))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
if (this._grid && index != null) {
this._grid.scrollToCell({
rowIndex: index,
columnIndex: 0
});
}
}
}
//
// Control
setGridRef = (ref) => {
this._grid = ref;
}
calculateGrid = (width = this.state.width, isSmallScreen) => {
const {
sortKey,
overviewOptions
} = this.props;
const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen);
const posterHeight = calculatePosterHeight(posterWidth);
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions);
this.setState({
width,
posterWidth,
posterHeight,
rowHeight
});
}
cellRenderer = ({ key, rowIndex, style }) => {
const {
items,
sortKey,
overviewOptions,
showRelativeDates,
shortDateFormat,
longDateFormat,
timeFormat,
isSmallScreen,
selectedState,
onSelectedChange
} = this.props;
const {
posterWidth,
posterHeight,
rowHeight
} = this.state;
const movie = items[rowIndex];
if (!movie) {
return null;
}
return (
<div
className={styles.container}
key={key}
style={style}
>
<DiscoverMovieItemConnector
key={movie.tmdbId}
component={DiscoverMovieOverviewConnector}
sortKey={sortKey}
posterWidth={posterWidth}
posterHeight={posterHeight}
rowHeight={rowHeight}
overviewOptions={overviewOptions}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
isSmallScreen={isSmallScreen}
movieId={movie.tmdbId}
isSelected={selectedState[movie.tmdbId]}
onSelectedChange={onSelectedChange}
/>
</div>
);
}
//
// Listeners
onMeasure = ({ width }) => {
this.calculateGrid(width, this.props.isSmallScreen);
}
//
// Render
render() {
const {
isSmallScreen,
scroller,
items,
selectedState
} = this.props;
const {
width,
rowHeight
} = this.state;
return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={isSmallScreen ? undefined : scroller}
>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return <div />;
}
return (
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={1}
columnWidth={width}
rowCount={items.length}
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
selectedState={selectedState}
scrollToAlignment={'start'}
isScrollingOptout={true}
/>
</div>
);
}
}
</WindowScroller>
</Measure>
);
}
}
DiscoverMovieOverviews.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
overviewOptions: PropTypes.object.isRequired,
jumpToCharacter: PropTypes.string,
scroller: PropTypes.instanceOf(Element).isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired,
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired
};
export default DiscoverMovieOverviews;

View File

@@ -1,25 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import DiscoverMovieOverviews from './DiscoverMovieOverviews';
function createMapStateToProps() {
return createSelector(
(state) => state.discoverMovie.overviewOptions,
createUISettingsSelector(),
createDimensionsSelector(),
(overviewOptions, uiSettings, dimensions) => {
return {
overviewOptions,
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat,
isSmallScreen: dimensions.isSmallScreen
};
}
);
}
export default connect(createMapStateToProps)(DiscoverMovieOverviews);

Some files were not shown because too many files have changed in this diff Show More