Various UI Fixes and Updates

Closes #188
Closes #185
Closes #187
This commit is contained in:
Qstick 2018-01-25 22:01:53 -05:00
commit 54e9f88648
89 changed files with 2354 additions and 995 deletions

View file

@ -119,6 +119,7 @@ class HistoryRow extends Component {
artistId={artist.id}
albumTitle={album.title}
showOpenArtistButton={true}
showOpenAlbumButton={true}
/>
</TableRowCell>
);

View file

@ -161,6 +161,7 @@ class QueueRow extends Component {
trackFileId={album.trackFileId}
albumTitle={album.title}
showOpenArtistButton={true}
showOpenAlbumButton={true}
/>
</TableRowCell>
);

View file

@ -78,6 +78,10 @@ class AddNewArtistSearchResult extends Component {
isSmallScreen
} = this.props;
const {
isNewAddArtistModalOpen
} = this.state;
const linkProps = isExistingArtist ? { to: `/artist/${foreignArtistId}` } : { onPress: this.onPress };
let albums = '1 Album';
@ -88,78 +92,78 @@ class AddNewArtistSearchResult extends Component {
const height = calculateHeight(230, isSmallScreen);
return (
<Link
className={styles.searchResult}
{...linkProps}
>
{
!isSmallScreen &&
<div>
<Link
className={styles.searchResult}
{...linkProps}
>
{
!isSmallScreen &&
<ArtistPoster
className={styles.poster}
images={images}
size={250}
/>
}
}
<div>
<div className={styles.name}>
{artistName}
<div>
<div className={styles.name}>
{artistName}
{
!name.contains(year) && !!year &&
{
!name.contains(year) && !!year &&
<span className={styles.year}>({year})</span>
}
}
{
!!disambiguation &&
{
!!disambiguation &&
<span className={styles.year}>({disambiguation})</span>
}
}
{
isExistingArtist &&
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title="Already in your library"
{
isExistingArtist &&
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title="Already in your library"
/>
}
</div>
<div>
<Label size={sizes.LARGE}>
<HeartRating
rating={ratings.value}
iconSize={13}
/>
}
</div>
</Label>
<div>
<Label size={sizes.LARGE}>
<HeartRating
rating={ratings.value}
iconSize={13}
/>
</Label>
{
!!artistType &&
<Label size={sizes.LARGE}>
{artistType}
</Label>
}
{
!!artistType &&
<Label size={sizes.LARGE}>
{artistType}
</Label>
}
{
!!albumCount &&
<Label size={sizes.LARGE}>
{albums}
</Label>
}
{
!!albumCount &&
<Label size={sizes.LARGE}>
{albums}
</Label>
}
{
status === 'ended' &&
<Label
kind={kinds.DANGER}
size={sizes.LARGE}
>
Ended
</Label>
}
</div>
{
status === 'ended' &&
<Label
kind={kinds.DANGER}
size={sizes.LARGE}
>
Ended
</Label>
}
</div>
<div>
<div
className={styles.overview}
style={{
@ -173,10 +177,10 @@ class AddNewArtistSearchResult extends Component {
/>
</div>
</div>
</div>
</Link>
<AddNewArtistModal
isOpen={this.state.isNewAddArtistModalOpen && !isExistingArtist}
isOpen={isNewAddArtistModalOpen && !isExistingArtist}
foreignArtistId={foreignArtistId}
artistName={artistName}
year={year}
@ -184,7 +188,7 @@ class AddNewArtistSearchResult extends Component {
images={images}
onModalClose={this.onAddArtistModalClose}
/>
</Link>
</div>
);
}
}

View file

@ -4,7 +4,6 @@ import ReactDOM from 'react-dom';
import TetherComponent from 'react-tether';
import { icons, kinds } from 'Helpers/Props';
import Icon from 'Components/Icon';
import SpinnerIcon from 'Components/SpinnerIcon';
import FormInputButton from 'Components/Form/FormInputButton';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';

View file

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

View file

@ -42,3 +42,7 @@
margin-right: auto;
}
.openButtons {
margin-right: auto;
}

View file

@ -45,17 +45,20 @@ class AlbumDetailsModalContent extends Component {
albumId,
artistName,
foreignArtistId,
foreignAlbumId,
artistMonitored,
albumTitle,
monitored,
isSaving,
showOpenArtistButton,
showOpenAlbumButton,
startInteractiveSearch,
onMonitorAlbumPress,
onModalClose
} = this.props;
const artistLink = `/artist/${foreignArtistId}`;
const albumLink = `/album/${foreignAlbumId}`;
return (
<ModalContent
@ -121,18 +124,30 @@ class AlbumDetailsModalContent extends Component {
</Tabs>
</ModalBody>
<ModalFooter>
{
showOpenArtistButton &&
<Button
className={styles.openArtistButton}
to={artistLink}
onPress={onModalClose}
>
Open Artist
</Button>
}
<ModalFooter >
<div className={styles.openButtons}>
{
showOpenArtistButton &&
<Button
className={styles.openArtistButton}
to={artistLink}
onPress={onModalClose}
>
Open Artist
</Button>
}
{
showOpenAlbumButton &&
<Button
className={styles.openAlbumButton}
to={albumLink}
onPress={onModalClose}
>
Open Album
</Button>
}
</div>
<Button
onPress={onModalClose}
>
@ -150,6 +165,7 @@ AlbumDetailsModalContent.propTypes = {
artistId: PropTypes.number.isRequired,
artistName: PropTypes.string.isRequired,
foreignArtistId: PropTypes.string.isRequired,
foreignAlbumId: PropTypes.string.isRequired,
artistMonitored: PropTypes.bool.isRequired,
releaseDate: PropTypes.string.isRequired,
albumLabel: PropTypes.arrayOf(PropTypes.string).isRequired,
@ -157,6 +173,7 @@ AlbumDetailsModalContent.propTypes = {
monitored: PropTypes.bool.isRequired,
isSaving: PropTypes.bool,
showOpenArtistButton: PropTypes.bool,
showOpenAlbumButton: PropTypes.bool,
selectedTab: PropTypes.string.isRequired,
startInteractiveSearch: PropTypes.bool.isRequired,
onMonitorAlbumPress: PropTypes.func.isRequired,

View file

@ -5,7 +5,6 @@ import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import AlbumDetailsModal from './AlbumDetailsModal';
import EditAlbumModalConnector from './Edit/EditAlbumModalConnector';
import styles from './AlbumSearchCell.css';
class AlbumSearchCell extends Component {
@ -17,8 +16,7 @@ class AlbumSearchCell extends Component {
super(props, context);
this.state = {
isDetailsModalOpen: false,
isEditAlbumModalOpen: false
isDetailsModalOpen: false
};
}
@ -33,14 +31,6 @@ class AlbumSearchCell extends Component {
this.setState({ isDetailsModalOpen: false });
}
onEditAlbumPress = () => {
this.setState({ isEditAlbumModalOpen: true });
}
onEditAlbumModalClose = () => {
this.setState({ isEditAlbumModalOpen: false });
}
//
// Render
@ -67,12 +57,6 @@ class AlbumSearchCell extends Component {
onPress={this.onManualSearchPress}
/>
<IconButton
name={icons.EDIT}
title="Edit Album"
onPress={this.onEditAlbumPress}
/>
<AlbumDetailsModal
isOpen={this.state.isDetailsModalOpen}
albumId={albumId}
@ -84,12 +68,6 @@ class AlbumSearchCell extends Component {
{...otherProps}
/>
<EditAlbumModalConnector
isOpen={this.state.isEditAlbumModalOpen}
albumId={albumId}
artistId={artistId}
onModalClose={this.onEditAlbumModalClose}
/>
</TableRowCell>
);
}

View file

@ -10,10 +10,9 @@ import AlbumSearchCell from './AlbumSearchCell';
function createMapStateToProps() {
return createSelector(
(state, { albumId }) => albumId,
(state, { sceneSeasonNumber }) => sceneSeasonNumber,
createArtistSelector(),
createCommandsSelector(),
(albumId, sceneSeasonNumber, artist, commands) => {
(albumId, artist, commands) => {
const isSearching = _.some(commands, (command) => {
const albumSearch = command.name === commandNames.ALBUM_SEARCH;

View file

@ -11,6 +11,7 @@ import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Label from 'Components/Label';
import AlbumCover from 'Album/AlbumCover';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import EditAlbumModalConnector from 'Album/Edit/EditAlbumModalConnector';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
@ -50,6 +51,7 @@ class AlbumDetails extends Component {
super(props, context);
this.state = {
isOrganizeModalOpen: false,
isArtistHistoryModalOpen: false,
isManageTracksOpen: false,
isEditAlbumModalOpen: false,
@ -62,6 +64,14 @@ class AlbumDetails extends Component {
//
// Listeners
onOrganizePress = () => {
this.setState({ isOrganizeModalOpen: true });
}
onOrganizeModalClose = () => {
this.setState({ isOrganizeModalOpen: false });
}
onEditAlbumPress = () => {
this.setState({ isEditAlbumModalOpen: true });
}
@ -135,6 +145,7 @@ class AlbumDetails extends Component {
} = this.props;
const {
isOrganizeModalOpen,
isArtistHistoryModalOpen,
isEditAlbumModalOpen,
isManageTracksOpen,
@ -164,6 +175,12 @@ class AlbumDetails extends Component {
<PageToolbarSeparator />
<PageToolbarButton
label="Preview Rename"
iconName={icons.ORGANIZE}
onPress={this.onOrganizePress}
/>
<PageToolbarButton
label="Manage Tracks"
iconName={icons.TRACK_FILE}
@ -364,6 +381,13 @@ class AlbumDetails extends Component {
</div>
<OrganizePreviewModalConnector
isOpen={isOrganizeModalOpen}
artistId={artist.id}
albumId={id}
onModalClose={this.onOrganizeModalClose}
/>
<TrackFileEditorModal
isOpen={isManageTracksOpen}
artistId={artist.id}

View file

@ -26,7 +26,7 @@ class EditAlbumModalContent extends Component {
}
//
//
// Render
render() {

View file

@ -2,41 +2,9 @@ import PropTypes from 'prop-types';
import React from 'react';
import DocumentTitle from 'react-document-title';
import { Provider } from 'react-redux';
import { Route, Redirect } from 'react-router-dom';
import { ConnectedRouter } from 'react-router-redux';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import PageConnector from 'Components/Page/PageConnector';
import ArtistIndexConnector from 'Artist/Index/ArtistIndexConnector';
import AddNewArtistConnector from 'AddArtist/AddNewArtist/AddNewArtistConnector';
import ImportArtist from 'AddArtist/ImportArtist/ImportArtist';
import ArtistEditorConnector from 'Artist/Editor/ArtistEditorConnector';
import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector';
import ArtistDetailsPageConnector from 'Artist/Details/ArtistDetailsPageConnector';
import AlbumDetailsPageConnector from 'Album/Details/AlbumDetailsPageConnector';
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import Settings from 'Settings/Settings';
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
import Profiles from 'Settings/Profiles/Profiles';
import Quality from 'Settings/Quality/Quality';
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
import DownloadClientSettings from 'Settings/DownloadClients/DownloadClientSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
import Status from 'System/Status/Status';
import TasksConnector from 'System/Tasks/TasksConnector';
import BackupsConnector from 'System/Backup/BackupsConnector';
import UpdatesConnector from 'System/Updates/UpdatesConnector';
import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs';
import AppRoutes from './AppRoutes';
function App({ store, history }) {
return (
@ -44,205 +12,7 @@ function App({ store, history }) {
<Provider store={store}>
<ConnectedRouter history={history}>
<PageConnector>
<Switch>
{/*
Artist
*/}
<Route
exact={true}
path="/"
component={ArtistIndexConnector}
/>
{
window.Sonarr.urlBase &&
<Route
exact={true}
path="/"
addUrlBase={false}
render={() => {
return (
<Redirect
to={getPathWithUrlBase('/')}
component={App}
/>
);
}}
/>
}
<Route
path="/add/new"
component={AddNewArtistConnector}
/>
<Route
path="/add/import"
component={ImportArtist}
/>
<Route
path="/artisteditor"
component={ArtistEditorConnector}
/>
<Route
path="/albumstudio"
component={AlbumStudioConnector}
/>
<Route
path="/artist/:foreignArtistId"
component={ArtistDetailsPageConnector}
/>
<Route
path="/album/:foreignAlbumId"
component={AlbumDetailsPageConnector}
/>
{/*
Calendar
*/}
<Route
path="/calendar"
component={CalendarPageConnector}
/>
{/*
Activity
*/}
<Route
path="/activity/history"
component={HistoryConnector}
/>
<Route
path="/activity/queue"
component={QueueConnector}
/>
<Route
path="/activity/blacklist"
component={BlacklistConnector}
/>
{/*
Wanted
*/}
<Route
path="/wanted/missing"
component={MissingConnector}
/>
<Route
path="/wanted/cutoffunmet"
component={CutoffUnmetConnector}
/>
{/*
Settings
*/}
<Route
exact={true}
path="/settings"
component={Settings}
/>
<Route
path="/settings/mediamanagement"
component={MediaManagementConnector}
/>
<Route
path="/settings/profiles"
component={Profiles}
/>
<Route
path="/settings/quality"
component={Quality}
/>
<Route
path="/settings/indexers"
component={IndexerSettings}
/>
<Route
path="/settings/downloadclients"
component={DownloadClientSettings}
/>
<Route
path="/settings/connect"
component={NotificationSettings}
/>
<Route
path="/settings/metadata"
component={MetadataSettings}
/>
<Route
path="/settings/general"
component={GeneralSettingsConnector}
/>
<Route
path="/settings/ui"
component={UISettingsConnector}
/>
{/*
System
*/}
<Route
path="/system/status"
component={Status}
/>
<Route
path="/system/tasks"
component={TasksConnector}
/>
<Route
path="/system/backup"
component={BackupsConnector}
/>
<Route
path="/system/updates"
component={UpdatesConnector}
/>
<Route
path="/system/events"
component={LogsTableConnector}
/>
<Route
path="/system/logs/files"
component={Logs}
/>
{/*
Not Found
*/}
<Route
path="*"
component={NotFound}
/>
</Switch>
<AppRoutes app={App} />
</PageConnector>
</ConnectedRouter>
</Provider>

View file

@ -0,0 +1,249 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import ArtistIndexConnector from 'Artist/Index/ArtistIndexConnector';
import AddNewArtistConnector from 'AddArtist/AddNewArtist/AddNewArtistConnector';
import ImportArtist from 'AddArtist/ImportArtist/ImportArtist';
import ArtistEditorConnector from 'Artist/Editor/ArtistEditorConnector';
import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector';
import ArtistDetailsPageConnector from 'Artist/Details/ArtistDetailsPageConnector';
import AlbumDetailsPageConnector from 'Album/Details/AlbumDetailsPageConnector';
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import Settings from 'Settings/Settings';
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
import Profiles from 'Settings/Profiles/Profiles';
import Quality from 'Settings/Quality/Quality';
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
import DownloadClientSettings from 'Settings/DownloadClients/DownloadClientSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
import Status from 'System/Status/Status';
import TasksConnector from 'System/Tasks/TasksConnector';
import BackupsConnector from 'System/Backup/BackupsConnector';
import UpdatesConnector from 'System/Updates/UpdatesConnector';
import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs';
function AppRoutes(props) {
const {
app
} = props;
return (
<Switch>
{/*
Artist
*/}
<Route
exact={true}
path="/"
component={ArtistIndexConnector}
/>
{
window.Lidarr.urlBase &&
<Route
exact={true}
path="/"
addUrlBase={false}
render={() => {
return (
<Redirect
to={getPathWithUrlBase('/')}
component={app}
/>
);
}}
/>
}
<Route
path="/add/new"
component={AddNewArtistConnector}
/>
<Route
path="/add/import"
component={ImportArtist}
/>
<Route
path="/artisteditor"
component={ArtistEditorConnector}
/>
<Route
path="/albumstudio"
component={AlbumStudioConnector}
/>
<Route
path="/artist/:foreignArtistId"
component={ArtistDetailsPageConnector}
/>
<Route
path="/album/:foreignAlbumId"
component={AlbumDetailsPageConnector}
/>
{/*
Calendar
*/}
<Route
path="/calendar"
component={CalendarPageConnector}
/>
{/*
Activity
*/}
<Route
path="/activity/history"
component={HistoryConnector}
/>
<Route
path="/activity/queue"
component={QueueConnector}
/>
<Route
path="/activity/blacklist"
component={BlacklistConnector}
/>
{/*
Wanted
*/}
<Route
path="/wanted/missing"
component={MissingConnector}
/>
<Route
path="/wanted/cutoffunmet"
component={CutoffUnmetConnector}
/>
{/*
Settings
*/}
<Route
exact={true}
path="/settings"
component={Settings}
/>
<Route
path="/settings/mediamanagement"
component={MediaManagementConnector}
/>
<Route
path="/settings/profiles"
component={Profiles}
/>
<Route
path="/settings/quality"
component={Quality}
/>
<Route
path="/settings/indexers"
component={IndexerSettings}
/>
<Route
path="/settings/downloadclients"
component={DownloadClientSettings}
/>
<Route
path="/settings/connect"
component={NotificationSettings}
/>
<Route
path="/settings/metadata"
component={MetadataSettings}
/>
<Route
path="/settings/general"
component={GeneralSettingsConnector}
/>
<Route
path="/settings/ui"
component={UISettingsConnector}
/>
{/*
System
*/}
<Route
path="/system/status"
component={Status}
/>
<Route
path="/system/tasks"
component={TasksConnector}
/>
<Route
path="/system/backup"
component={BackupsConnector}
/>
<Route
path="/system/updates"
component={UpdatesConnector}
/>
<Route
path="/system/events"
component={LogsTableConnector}
/>
<Route
path="/system/logs/files"
component={Logs}
/>
{/*
Not Found
*/}
<Route
path="*"
component={NotFound}
/>
</Switch>
);
}
AppRoutes.propTypes = {
app: PropTypes.func.isRequired
};
export default AppRoutes;

View file

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

View file

@ -38,7 +38,7 @@ class ArtistDetailsPageConnector extends Component {
componentDidUpdate(prevProps) {
if (!this.props.foreignArtistId) {
this.props.push(`${window.Sonarr.urlBase}/`);
this.props.push(`${window.Lidarr.urlBase}/`);
return;
}
}

View file

@ -51,7 +51,7 @@ class EditArtistModalContent extends Component {
this.props.onSavePress(true);
}
//
//
// Render
render() {

View file

@ -5,7 +5,6 @@ import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import IconButton from 'Components/Link/IconButton';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ArtistPoster from 'Artist/ArtistPoster';

View file

@ -5,6 +5,5 @@
}
.statusIcon {
margin-right: 6px;
width: 20px;
width: 20px !important;
}

View file

@ -17,16 +17,9 @@ function createMapStateToProps() {
);
}
function createMapDispatchToProps(dispatch) {
return {
onNavigatePrevious() {
dispatch(gotoCalendarPreviousRange());
},
const mapDispatchToProps = {
onNavigatePrevious: gotoCalendarPreviousRange,
onNavigateNext: gotoCalendarNextRange
};
onNavigateNext() {
dispatch(gotoCalendarNextRange());
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(CalendarDays);
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarDays);

View file

@ -22,7 +22,7 @@ function getUrls(state) {
tags
} = state;
let icalUrl = `${window.location.host}${window.Sonarr.urlBase}/feed/v1/calendar/Lidarr.ics?`;
let icalUrl = `${window.location.host}${window.Lidarr.urlBase}/feed/v1/calendar/Lidarr.ics?`;
if (unmonitored) {
icalUrl += 'unmonitored=true&';
@ -40,7 +40,7 @@ function getUrls(state) {
icalUrl += `tags=${tags.toString()}&`;
}
icalUrl += `apikey=${window.Sonarr.apiKey}`;
icalUrl += `apikey=${window.Lidarr.apiKey}`;
const iCalHttpUrl = `${window.location.protocol}//${icalUrl}`;
const iCalWebCalUrl = `webcal://${icalUrl}`;

View file

@ -14,3 +14,8 @@
.scroller {
margin-top: 20px;
}
.loading {
display: inline-block;
margin-right: auto;
}

View file

@ -3,11 +3,12 @@ import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { scrollDirections } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Scroller from 'Components/Scroller/Scroller';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import Scroller from 'Components/Scroller/Scroller';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import PathInput from 'Components/Form/PathInput';
@ -43,12 +44,15 @@ class FileBrowserModalContent extends Component {
};
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps, prevState) {
const {
currentPath
} = this.props;
if (currentPath !== this.state.currentPath) {
if (
currentPath !== this.state.currentPath &&
currentPath !== prevState.currentPath
) {
this.setState({ currentPath });
this._scrollerNode.scrollTop = 0;
}
@ -91,6 +95,9 @@ class FileBrowserModalContent extends Component {
render() {
const {
isFetching,
isPopulated,
error,
parent,
directories,
files,
@ -125,61 +132,77 @@ class FileBrowserModalContent extends Component {
ref={this.setScrollerRef}
className={styles.scroller}
>
<Table columns={columns}>
<TableBody>
{
emptyParent &&
<FileBrowserRow
type="computer"
name="My Computer"
path={parent}
onPress={this.onRowPress}
/>
}
{
!!error &&
<div>Error loading contents</div>
}
{
!emptyParent && parent &&
<FileBrowserRow
type="parent"
name="..."
path={parent}
onPress={this.onRowPress}
/>
}
{
directories.map((directory) => {
return (
{
isPopulated && !error &&
<Table columns={columns}>
<TableBody>
{
emptyParent &&
<FileBrowserRow
key={directory.path}
type={directory.type}
name={directory.name}
path={directory.path}
type="computer"
name="My Computer"
path={parent}
onPress={this.onRowPress}
/>
);
})
}
}
{
files.map((file) => {
return (
{
!emptyParent && parent &&
<FileBrowserRow
key={file.path}
type={file.type}
name={file.name}
path={file.path}
type="parent"
name="..."
path={parent}
onPress={this.onRowPress}
/>
);
})
}
</TableBody>
</Table>
}
{
directories.map((directory) => {
return (
<FileBrowserRow
key={directory.path}
type={directory.type}
name={directory.name}
path={directory.path}
onPress={this.onRowPress}
/>
);
})
}
{
files.map((file) => {
return (
<FileBrowserRow
key={file.path}
type={file.type}
name={file.name}
path={file.path}
onPress={this.onRowPress}
/>
);
})
}
</TableBody>
</Table>
}
</Scroller>
</ModalBody>
<ModalFooter>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
<Button
onPress={onModalClose}
>
@ -200,6 +223,9 @@ class FileBrowserModalContent extends Component {
FileBrowserModalContent.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
parent: PropTypes.string,
currentPath: PropTypes.string.isRequired,
directories: PropTypes.arrayOf(PropTypes.object).isRequired,

View file

@ -11,6 +11,9 @@ function createMapStateToProps() {
(state) => state.paths,
(paths) => {
const {
isFetching,
isPopulated,
error,
parent,
currentPath,
directories,
@ -22,6 +25,9 @@ function createMapStateToProps() {
});
return {
isFetching,
isPopulated,
error,
parent,
currentPath,
directories,

View file

@ -8,12 +8,23 @@ class NumberInput extends Component {
// Listeners
onChange = ({ name, value }) => {
const {
min,
max
} = this.props;
let newValue = null;
if (value) {
newValue = this.props.isFloat ? parseFloat(value) : parseInt(value);
}
if (min != null && newValue < min) {
newValue = min;
} else if (max != null && newValue > max) {
newValue = max;
}
this.props.onChange({
name,
value: newValue
@ -40,6 +51,8 @@ class NumberInput extends Component {
NumberInput.propTypes = {
value: PropTypes.number,
min: PropTypes.number,
max: PropTypes.number,
isFloat: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired
};

View file

@ -98,7 +98,7 @@ class ClipboardButton extends Component {
className={styles.button}
{...otherProps}
>
<span className={showStateIcon && styles.showStateIcon}>
<span className={showStateIcon ? styles.showStateIcon : undefined}>
{
showSuccess &&
<span className={styles.stateIconContainer}>

View file

@ -41,7 +41,8 @@ IconButton.propTypes = {
};
IconButton.defaultProps = {
className: styles.button
className: styles.button,
size: 12
};
export default IconButton;

View file

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

View file

@ -32,6 +32,6 @@
.label {
left: 100%;
opacity: 0;
visibility: hidden;
}
}

View file

@ -129,7 +129,7 @@ class SpinnerErrorButton extends Component {
isSpinning={isSpinning}
{...otherProps}
>
<span className={showIcon && styles.showIcon}>
<span className={showIcon ? styles.showIcon : undefined}>
{
showIcon &&
<span className={styles.iconContainer}>

View file

@ -16,6 +16,7 @@ function SpinnerIconButton(props) {
<IconButton
name={isSpinning ? (spinningName || name) : name}
isDisabled={isDisabled || isSpinning}
isSpinning={isSpinning}
{...otherProps}
/>
);

View file

@ -1,7 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Portal from 'react-portal';
import classNames from 'classnames';
import elementClass from 'element-class';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
@ -27,6 +26,8 @@ class Modal extends Component {
constructor(props, context) {
super(props, context);
this._node = document.getElementById('modal-root');
this._backgroundRef = null;
this._modalId = getUniqueElememtId();
}
@ -57,6 +58,10 @@ class Modal extends Component {
//
// Control
_setBackgroundRef = (ref) => {
this._backgroundRef = ref;
}
_openModal() {
openModals.push(this._modalId);
window.addEventListener('keydown', this.onKeyDown);
@ -79,9 +84,9 @@ class Modal extends Component {
const targetElement = this._findEventTarget(event);
if (targetElement) {
const modalElement = ReactDOM.findDOMNode(this.refs.modal);
const backgroundElement = ReactDOM.findDOMNode(this._backgroundRef);
return !modalElement || !modalElement.contains(targetElement);
return backgroundElement.isEqualNode(targetElement);
}
return false;
@ -138,10 +143,6 @@ class Modal extends Component {
}
}
onClosePress = (event) => {
this.props.onModalClose();
}
//
// Render
@ -155,36 +156,32 @@ class Modal extends Component {
isOpen
} = this.props;
return (
<Portal
isOpened={isOpen}
if (!isOpen) {
return null;
}
return ReactDOM.createPortal(
<div
className={styles.modalContainer}
>
<div>
{
isOpen &&
<div
className={styles.modalContainer}
>
<div
className={backdropClassName}
onMouseDown={this.onBackdropBeginPress}
onMouseUp={this.onBackdropEndPress}
>
<div
ref="modal"
className={classNames(
className,
styles[size]
)}
style={style}
>
{children}
</div>
</div>
</div>
}
<div
ref={this._setBackgroundRef}
className={backdropClassName}
onMouseDown={this.onBackdropBeginPress}
onMouseUp={this.onBackdropEndPress}
>
<div
className={classNames(
className,
styles[size]
)}
style={style}
>
{children}
</div>
</div>
</Portal>
</div>,
this._node
);
}
}

View file

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

View file

@ -19,11 +19,11 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
onGoToArtist(foreignArtistId) {
dispatch(push(`${window.Sonarr.urlBase}/artist/${foreignArtistId}`));
dispatch(push(`${window.Lidarr.urlBase}/artist/${foreignArtistId}`));
},
onGoToAddNewArtist(query) {
dispatch(push(`${window.Sonarr.urlBase}/add/new?term=${encodeURIComponent(query)}`));
dispatch(push(`${window.Lidarr.urlBase}/add/new?term=${encodeURIComponent(query)}`));
}
};
}

View file

@ -51,10 +51,10 @@ class PageHeader extends Component {
return (
<div className={styles.header}>
<div className={styles.logoContainer}>
<Link to={`${window.Sonarr.urlBase}/`}>
<Link to={`${window.Lidarr.urlBase}/`}>
<img
className={styles.logo}
src={`${window.Sonarr.urlBase}/Content/Images/logo.svg`}
src={`${window.Lidarr.urlBase}/Content/Images/logo.svg`}
/>
</Link>
</div>
@ -74,6 +74,7 @@ class PageHeader extends Component {
className={styles.donate}
name={icons.HEART}
to="https://www.paypal.me/Lidarr"
size={14}
/>
<PageHeaderActionsMenuConnector
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}

View file

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

View file

@ -15,7 +15,7 @@ import LoadingPage from './LoadingPage';
import Page from './Page';
function testLocalStorage() {
const key = 'sonarrTest';
const key = 'lidarrTest';
try {
localStorage.setItem(key, key);
@ -64,7 +64,7 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
dispatchFetchSeries() {
dispatchFetchArtist() {
dispatch(fetchArtist());
},
dispatchFetchTags() {
@ -109,7 +109,7 @@ class PageConnector extends Component {
componentDidMount() {
if (!this.props.isPopulated) {
this.props.dispatchFetchSeries();
this.props.dispatchFetchArtist();
this.props.dispatchFetchTags();
this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchLanguageProfiles();
@ -133,7 +133,7 @@ class PageConnector extends Component {
const {
isPopulated,
hasError,
dispatchFetchSeries,
dispatchFetchArtist,
dispatchFetchTags,
dispatchFetchQualityProfiles,
dispatchFetchLanguageProfiles,
@ -171,7 +171,7 @@ PageConnector.propTypes = {
isPopulated: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired,
dispatchFetchSeries: PropTypes.func.isRequired,
dispatchFetchArtist: PropTypes.func.isRequired,
dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchLanguageProfiles: PropTypes.func.isRequired,

View file

@ -415,7 +415,7 @@ class PageSidebar extends Component {
transform
} = this.state;
const urlBase = window.Sonarr.urlBase;
const urlBase = window.Lidarr.urlBase;
const pathname = urlBase ? location.pathname.substr(urlBase.length) || '/' : location.pathname;
const activeParent = getActiveParent(pathname);

View file

@ -80,7 +80,7 @@ class SignalRConnector extends Component {
componentDidMount() {
console.log('Starting signalR');
this.signalRconnection = $.connection('/signalr', { apiKey: window.Sonarr.apiKey });
this.signalRconnection = $.connection('/signalr', { apiKey: window.Lidarr.apiKey });
this.signalRconnection.stateChanged(this.onStateChanged);
this.signalRconnection.received(this.onReceived);
@ -232,11 +232,12 @@ class SignalRConnector extends Component {
}
handleTrackFile = (body) => {
const section = 'trackFiles';
if (body.action === 'updated') {
this.props.updateItem({
section: 'trackFiles',
...body.resource
});
this.props.updateItem({ section, ...body.resource });
} else if (body.action === 'deleted') {
this.props.removeItem({ section, id: body.resource.id });
}
}
@ -335,7 +336,7 @@ class SignalRConnector extends Component {
}
onReconnecting = () => {
if (window.Sonarr.unloading) {
if (window.Lidarr.unloading) {
return;
}
@ -349,7 +350,7 @@ class SignalRConnector extends Component {
}
onDisconnected = () => {
if (window.Sonarr.unloading) {
if (window.Lidarr.unloading) {
return;
}

View file

@ -14,14 +14,15 @@ function SpinnerIcon(props) {
return (
<Icon
name={isSpinning ? (spinningName || name) : name}
isSpinning={isSpinning}
{...otherProps}
/>
);
}
SpinnerIcon.propTypes = {
name: PropTypes.string.isRequired,
spinningName: PropTypes.string.isRequired,
name: PropTypes.object.isRequired,
spinningName: PropTypes.object.isRequired,
isSpinning: PropTypes.bool.isRequired
};

View file

@ -18,7 +18,7 @@ function TableOptionsColumn(props) {
} = props;
return (
<div className={!isModifiable && styles.notDragable}>
<div className={isModifiable ? undefined : styles.notDragable}>
<div
className={classNames(
styles.column,

View file

@ -0,0 +1,31 @@
.button {
composes: toolbarButton from 'Components/Page/Toolbar/PageToolbarButton.css';
position: relative;
}
.labelContainer {
composes: labelContainer from 'Components/Page/Toolbar/PageToolbarButton.css';
}
.label {
composes: label from 'Components/Page/Toolbar/PageToolbarButton.css';
}
.indicatorContainer {
position: absolute;
top: 10px;
right: 12px;
}
.indicatorBackground {
color: $themeDarkColor;
}
.enabled {
color: $successColor;
}
.disabled {
color: $dangerColor;
}

View file

@ -0,0 +1,59 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import styles from './AdvancedSettingsButton.css';
function AdvancedSettingsButton(props) {
const {
advancedSettings,
onAdvancedSettingsPress
} = props;
return (
<Link
className={styles.button}
title={advancedSettings ? 'Shown, click to hide' : 'Hidden, click to show'}
onPress={onAdvancedSettingsPress}
>
<Icon
name={icons.ADVANCED_SETTINGS}
size={21}
/>
<span
className={classNames(
styles.indicatorContainer,
'fa-layers fa-fw'
)}
>
<Icon
className={styles.indicatorBackground}
name={icons.CIRCLE}
size={16}
/>
<Icon
className={advancedSettings ? styles.enabled : styles.disabled}
name={advancedSettings ? icons.CHECK : icons.CLOSE}
size={10}
/>
</span>
<div className={styles.labelContainer}>
<div className={styles.label}>
{advancedSettings ? 'Hide Advanced' : 'Show Advanced'}
</div>
</div>
</Link>
);
}
AdvancedSettingsButton.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired
};
export default AdvancedSettingsButton;

View file

@ -14,7 +14,10 @@ class DownloadClientSettings extends Component {
constructor(props, context) {
super(props, context);
this._saveCallback = null;
this.state = {
isSaving: false,
hasPendingChanges: false
};
}
@ -22,28 +25,34 @@ class DownloadClientSettings extends Component {
//
// Listeners
setDownloadClientOptionsRef = (ref) => {
this._downloadClientOptions = ref;
onChildMounted = (saveCallback) => {
this._saveCallback = saveCallback;
}
onHasPendingChange = (hasPendingChanges) => {
this.setState({
hasPendingChanges
});
onChildStateChange = (payload) => {
this.setState(payload);
}
onSavePress = () => {
this._downloadClientOptions.getWrappedInstance().save();
if (this._saveCallback) {
this._saveCallback();
}
}
//
// Render
render() {
const {
isSaving,
hasPendingChanges
} = this.state;
return (
<PageContent title="Download Client Settings">
<SettingsToolbarConnector
hasPendingChanges={this.state.hasPendingChanges}
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
onSavePress={this.onSavePress}
/>
@ -51,8 +60,8 @@ class DownloadClientSettings extends Component {
<DownloadClientsConnector />
<DownloadClientOptionsConnector
ref={this.setDownloadClientOptionsRef}
onHasPendingChange={this.onHasPendingChange}
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
<RemotePathMappingsConnector />

View file

@ -60,6 +60,7 @@ class DownloadClient extends Component {
return (
<Card
className={styles.downloadClient}
overlayContent={true}
onPress={this.onEditDownloadClientPress}
>
<div className={styles.name}>

View file

@ -21,10 +21,10 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchDownloadClientOptions,
setDownloadClientOptionsValue,
saveDownloadClientOptions,
clearPendingChanges
dispatchFetchDownloadClientOptions: fetchDownloadClientOptions,
dispatchSetDownloadClientOptionsValue: setDownloadClientOptionsValue,
dispatchSaveDownloadClientOptions: saveDownloadClientOptions,
dispatchClearPendingChanges: clearPendingChanges
};
class DownloadClientOptionsConnector extends Component {
@ -33,31 +33,43 @@ class DownloadClientOptionsConnector extends Component {
// Lifecycle
componentDidMount() {
this.props.fetchDownloadClientOptions();
const {
dispatchFetchDownloadClientOptions,
dispatchSaveDownloadClientOptions,
onChildMounted
} = this.props;
dispatchFetchDownloadClientOptions();
onChildMounted(dispatchSaveDownloadClientOptions);
}
componentDidUpdate(prevProps) {
if (this.props.hasPendingChanges !== prevProps.hasPendingChanges) {
this.props.onHasPendingChange(this.props.hasPendingChanges);
const {
hasPendingChanges,
isSaving,
onChildStateChange
} = this.props;
if (
prevProps.isSaving !== isSaving ||
prevProps.hasPendingChanges !== hasPendingChanges
) {
onChildStateChange({
isSaving,
hasPendingChanges
});
}
}
componentWillUnmount() {
this.props.clearPendingChanges({ section: this.props.section });
}
//
// Control
save = () => {
this.props.saveDownloadClientOptions();
this.props.dispatchClearPendingChanges({ section: this.props.section });
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setDownloadClientOptionsValue({ name, value });
this.props.dispatchSetDownloadClientOptionsValue({ name, value });
}
//
@ -75,18 +87,20 @@ class DownloadClientOptionsConnector extends Component {
DownloadClientOptionsConnector.propTypes = {
section: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired,
fetchDownloadClientOptions: PropTypes.func.isRequired,
setDownloadClientOptionsValue: PropTypes.func.isRequired,
saveDownloadClientOptions: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired,
onHasPendingChange: PropTypes.func.isRequired
dispatchFetchDownloadClientOptions: PropTypes.func.isRequired,
dispatchSetDownloadClientOptionsValue: PropTypes.func.isRequired,
dispatchSaveDownloadClientOptions: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
onChildMounted: PropTypes.func.isRequired,
onChildStateChange: PropTypes.func.isRequired
};
export default connectSection(
createMapStateToProps,
mapDispatchToProps,
undefined,
{ withRef: true },
undefined,
{ section: 'settings.downloadClientOptions' }
)(DownloadClientOptionsConnector);

View file

@ -49,6 +49,8 @@ function HostSettings(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="port"
min={1}
max={65535}
helpTextWarning="Requires restart to take effect"
onChange={onInputChange}
{...port}
@ -95,6 +97,8 @@ function HostSettings(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="sslPort"
min={1}
max={65535}
helpTextWarning="Requires restart to take effect"
onChange={onInputChange}
{...sslPort}

View file

@ -74,6 +74,8 @@ function ProxySettings(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="proxyPort"
min={1}
max={65535}
onChange={onInputChange}
{...proxyPort}
/>

View file

@ -14,7 +14,10 @@ class IndexerSettings extends Component {
constructor(props, context) {
super(props, context);
this._saveCallback = null;
this.state = {
isSaving: false,
hasPendingChanges: false
};
}
@ -22,28 +25,34 @@ class IndexerSettings extends Component {
//
// Listeners
setIndexerOptionsRef = (ref) => {
this._indexerOptions = ref;
onChildMounted = (saveCallback) => {
this._saveCallback = saveCallback;
}
onHasPendingChange = (hasPendingChanges) => {
this.setState({
hasPendingChanges
});
onChildStateChange = (payload) => {
this.setState(payload);
}
onSavePress = () => {
this._indexerOptions.getWrappedInstance().save();
if (this._saveCallback) {
this._saveCallback();
}
}
//
// Render
render() {
const {
isSaving,
hasPendingChanges
} = this.state;
return (
<PageContent title="Indexer Settings">
<SettingsToolbarConnector
hasPendingChanges={this.state.hasPendingChanges}
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
onSavePress={this.onSavePress}
/>
@ -51,8 +60,8 @@ class IndexerSettings extends Component {
<IndexersConnector />
<IndexerOptionsConnector
ref={this.setIndexerOptionsRef}
onHasPendingChange={this.onHasPendingChange}
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
<RestrictionsConnector />

View file

@ -76,6 +76,7 @@ class Indexer extends Component {
return (
<Card
className={styles.indexer}
overlayContent={true}
onPress={this.onEditIndexerPress}
>
<div className={styles.name}>

View file

@ -41,6 +41,7 @@ function IndexerOptions(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="minimumAge"
min={0}
helpText="Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."
onChange={onInputChange}
{...settings.minimumAge}
@ -53,6 +54,7 @@ function IndexerOptions(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="maximumSize"
min={0}
helpText="Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited."
onChange={onInputChange}
{...settings.maximumSize}
@ -65,6 +67,7 @@ function IndexerOptions(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="retention"
min={0}
helpText="Usenet only: Set to zero to set for unlimited retention"
onChange={onInputChange}
{...settings.retention}
@ -80,6 +83,7 @@ function IndexerOptions(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="rssSyncInterval"
min={0}
helpText="Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"
helpTextWarning="This will apply to all indexers, please follow the rules set forth by them"
helpLink="https://github.com/Sonarr/Sonarr/wiki/RSS-Sync"

View file

@ -21,10 +21,10 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchIndexerOptions,
setIndexerOptionsValue,
saveIndexerOptions,
clearPendingChanges
dispatchFetchIndexerOptions: fetchIndexerOptions,
dispatchSetIndexerOptionsValue: setIndexerOptionsValue,
dispatchSaveIndexerOptions: saveIndexerOptions,
dispatchClearPendingChanges: clearPendingChanges
};
class IndexerOptionsConnector extends Component {
@ -33,31 +33,43 @@ class IndexerOptionsConnector extends Component {
// Lifecycle
componentDidMount() {
this.props.fetchIndexerOptions();
const {
dispatchFetchIndexerOptions,
dispatchSaveIndexerOptions,
onChildMounted
} = this.props;
dispatchFetchIndexerOptions();
onChildMounted(dispatchSaveIndexerOptions);
}
componentDidUpdate(prevProps) {
if (this.props.hasPendingChanges !== prevProps.hasPendingChanges) {
this.props.onHasPendingChange(this.props.hasPendingChanges);
const {
hasPendingChanges,
isSaving,
onChildStateChange
} = this.props;
if (
prevProps.isSaving !== isSaving ||
prevProps.hasPendingChanges !== hasPendingChanges
) {
onChildStateChange({
isSaving,
hasPendingChanges
});
}
}
componentWillUnmount() {
this.props.clearPendingChanges({ section: this.props.section });
}
//
// Control
save = () => {
this.props.saveIndexerOptions();
this.props.dispatchClearPendingChanges({ section: this.props.section });
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setIndexerOptionsValue({ name, value });
this.props.dispatchSetIndexerOptionsValue({ name, value });
}
//
@ -75,18 +87,20 @@ class IndexerOptionsConnector extends Component {
IndexerOptionsConnector.propTypes = {
section: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired,
fetchIndexerOptions: PropTypes.func.isRequired,
setIndexerOptionsValue: PropTypes.func.isRequired,
saveIndexerOptions: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired,
onHasPendingChange: PropTypes.func.isRequired
dispatchFetchIndexerOptions: PropTypes.func.isRequired,
dispatchSetIndexerOptionsValue: PropTypes.func.isRequired,
dispatchSaveIndexerOptions: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
onChildMounted: PropTypes.func.isRequired,
onChildStateChange: PropTypes.func.isRequired
};
export default connectSection(
createMapStateToProps,
mapDispatchToProps,
undefined,
{ withRef: true },
undefined,
{ section: 'settings.indexerOptions' }
)(IndexerOptionsConnector);

View file

@ -64,6 +64,7 @@ class Restriction extends Component {
return (
<Card
className={styles.restriction}
overlayContent={true}
onPress={this.onEditRestrictionPress}
>
<div>

View file

@ -52,6 +52,7 @@ class Metadata extends Component {
return (
<Card
className={styles.metadata}
overlayContent={true}
onPress={this.onEditMetadataPress}
>
<div className={styles.name}>

View file

@ -79,6 +79,7 @@ class Notification extends Component {
return (
<Card
className={styles.notification}
overlayContent={true}
onPress={this.onEditNotificationPress}
>
<div className={styles.name}>

View file

@ -101,7 +101,7 @@ function EditLanguageProfileModalContent(props) {
id &&
<div
className={styles.deleteButtonContainer}
title={isInUse && 'Can\'t delete a language profile that is attached to a artist'}
title={isInUse ? 'Can\'t delete a language profile that is attached to a artist' : undefined}
>
<Button
kind={kinds.DANGER}

View file

@ -97,7 +97,7 @@ function EditMetadataProfileModalContent(props) {
id &&
<div
className={styles.deleteButtonContainer}
title={isInUse && 'Can\'t delete a metadata profile that is attached to a artist'}
title={isInUse ? 'Can\'t delete a metadata profile that is attached to a artist' : undefined}
>
<Button
kind={kinds.DANGER}

View file

@ -200,7 +200,7 @@ class EditQualityProfileModalContent extends Component {
id &&
<div
className={styles.deleteButtonContainer}
title={isInUse && 'Can\'t delete a quality profile that is attached to a artist'}
title={isInUse ? 'Can\'t delete a quality profile that is attached to a artist' : undefined}
>
<Button
kind={kinds.DANGER}

View file

@ -28,6 +28,29 @@ function getValue(value) {
class QualityDefinition extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._forceUpdateTimeout = null;
}
componentDidMount() {
// A hack to deal with a bug in the slider component until a fix for it
// lands and an updated version is available.
// See: https://github.com/mpowaga/react-slider/issues/115
this._forceUpdateTimeout = setTimeout(() => this.forceUpdate(), 1);
}
componentWillUnmount() {
if (this._forceUpdateTimeout) {
clearTimeout(this._forceUpdateTimeout);
}
}
//
// Listeners
@ -131,6 +154,8 @@ class QualityDefinition extends Component {
<NumberInput
className={styles.sizeInput}
name={`${id}.min`}
min={slider.min}
max={maxSize ? maxSize - 10 : slider.max - 10}
value={minSize || slider.min}
isFloat={true}
onChange={this.onMinSizeChange}
@ -143,6 +168,7 @@ class QualityDefinition extends Component {
<NumberInput
className={styles.sizeInput}
name={`${id}.max`}
min={minSize + 10}
value={maxSize || slider.max}
isFloat={true}
onChange={this.onMaxSizeChange}

View file

@ -40,7 +40,7 @@ class QualityDefinitionConnector extends Component {
this.props.setQualityDefinitionValue({ id, name: 'minSize', value: minSize });
}
if (minSize !== currentMaxSize) {
if (maxSize !== currentMaxSize) {
this.props.setQualityDefinitionValue({ id, name: 'maxSize', value: maxSize });
}
}

View file

@ -26,8 +26,8 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchQualityDefinitions,
saveQualityDefinitions
dispatchFetchQualityDefinitions: fetchQualityDefinitions,
dispatchSaveQualityDefinitions: saveQualityDefinitions
};
class QualityDefinitionsConnector extends Component {
@ -36,26 +36,36 @@ class QualityDefinitionsConnector extends Component {
// Lifecycle
componentDidMount() {
this.props.fetchQualityDefinitions();
this.props.dispatchFetchQualityDefinitions();
const {
dispatchFetchQualityDefinitions,
dispatchSaveQualityDefinitions,
onChildMounted
} = this.props;
dispatchFetchQualityDefinitions();
onChildMounted(dispatchSaveQualityDefinitions);
}
componentDidUpdate(prevProps) {
const {
hasPendingChanges
hasPendingChanges,
isSaving,
onChildStateChange
} = this.props;
if (hasPendingChanges !== prevProps.hasPendingChanges) {
this.props.onHasPendingChange(hasPendingChanges);
if (
prevProps.isSaving !== isSaving ||
prevProps.hasPendingChanges !== hasPendingChanges
) {
onChildStateChange({
isSaving,
hasPendingChanges
});
}
}
//
// Control
save = () => {
this.props.saveQualityDefinitions();
}
//
// Render
@ -69,10 +79,12 @@ class QualityDefinitionsConnector extends Component {
}
QualityDefinitionsConnector.propTypes = {
isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired,
fetchQualityDefinitions: PropTypes.func.isRequired,
saveQualityDefinitions: PropTypes.func.isRequired,
onHasPendingChange: PropTypes.func.isRequired
dispatchFetchQualityDefinitions: PropTypes.func.isRequired,
dispatchSaveQualityDefinitions: PropTypes.func.isRequired,
onChildMounted: PropTypes.func.isRequired,
onChildStateChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps, null, { withRef: true })(QualityDefinitionsConnector);

View file

@ -12,7 +12,10 @@ class Quality extends Component {
constructor(props, context) {
super(props, context);
this._saveCallback = null;
this.state = {
isSaving: false,
hasPendingChanges: false
};
}
@ -20,35 +23,41 @@ class Quality extends Component {
//
// Listeners
setQualityDefinitionsRef = (ref) => {
this._qualityDefinitions = ref;
onChildMounted = (saveCallback) => {
this._saveCallback = saveCallback;
}
onHasPendingChange = (hasPendingChanges) => {
this.setState({
hasPendingChanges
});
onChildStateChange = (payload) => {
this.setState(payload);
}
onSavePress = () => {
this._qualityDefinitions.getWrappedInstance().save();
if (this._saveCallback) {
this._saveCallback();
}
}
//
// Render
render() {
const {
isSaving,
hasPendingChanges
} = this.state;
return (
<PageContent title="Quality Settings">
<SettingsToolbarConnector
hasPendingChanges={this.state.hasPendingChanges}
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
onSavePress={this.onSavePress}
/>
<PageContentBodyConnector>
<QualityDefinitionsConnector
ref={this.setQualityDefinitionsRef}
onHasPendingChange={this.onHasPendingChange}
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
</PageContentBodyConnector>
</PageContent>

View file

@ -1,7 +0,0 @@
.advancedSettings {
composes: toolbarButton from 'Components/Page/Toolbar/PageToolbarButton.css';
}
.advancedSettingsEnabled {
color: $toobarButtonHoverColor;
}

View file

@ -1,13 +1,12 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PendingChangesModal from './PendingChangesModal';
import styles from './SettingsToolbar.css';
import AdvancedSettingsButton from './AdvancedSettingsButton';
class SettingsToolbar extends Component {
@ -53,14 +52,9 @@ class SettingsToolbar extends Component {
return (
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={advancedSettings ? 'Hide Advanced' : 'Show Advanced'}
className={classNames(
styles.advancedSettings,
advancedSettings && styles.advancedSettingsEnabled
)}
iconName={icons.ADVANCED_SETTINGS}
onPress={onAdvancedSettingsPress}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
/>
{

View file

@ -1,4 +1,4 @@
if (window.Sonarr.analytics) {
if (window.Lidarr.analytics) {
const d = document;
const g = d.createElement('script');
const s = d.getElementsByTagName('script')[0];

View file

@ -7,7 +7,7 @@ import updateSectionState from 'Utilities/State/updateSectionState';
import { createThunk } from 'Store/thunks';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createSaveHandler from 'Store/Actions/Creators/createSaveHandler';
import { clearPendingChanges, update } from 'Store/Actions/baseActions';
import { clearPendingChanges, set, update } from 'Store/Actions/baseActions';
//
// Variables
@ -70,6 +70,11 @@ export default {
return;
}
dispatch(set({
section,
isSaving: true
}));
const promise = $.ajax({
method: 'PUT',
url: '/qualityDefinition/update',
@ -78,10 +83,24 @@ export default {
promise.done((data) => {
dispatch(batchActions([
set({
section,
isSaving: false,
saveError: null
}),
update({ section, data }),
clearPendingChanges({ section })
]));
});
promise.fail((xhr) => {
dispatch(set({
section,
isSaving: false,
saveError: xhr
}));
});
}
},

View file

@ -31,7 +31,7 @@ export const defaultState = {
messages: {
items: []
},
version: window.Sonarr.version,
version: window.Lidarr.version,
isUpdated: false,
isConnected: true,
isReconnecting: false,

View file

@ -176,25 +176,23 @@ export const actionHandlers = handleThunks({
[FETCH_QUEUE_STATUS]: createFetchHandler(status, '/queue/status'),
[FETCH_QUEUE_DETAILS]: function(payload) {
return function(dispatch, getState) {
let params = payload;
[FETCH_QUEUE_DETAILS]: function(getState, payload, dispatch) {
let params = payload;
// If the payload params are empty try to get params from state.
// If the payload params are empty try to get params from state.
if (params && !_.isEmpty(params)) {
dispatch(set({ section: details, params }));
} else {
params = getState().queue.details.params;
}
if (params && !_.isEmpty(params)) {
dispatch(set({ section: details, params }));
} else {
params = getState().queue.details.params;
}
// Ensure there are params before trying to fetch the queue
// so we don't make a bad request to the server.
// Ensure there are params before trying to fetch the queue
// so we don't make a bad request to the server.
if (params && !_.isEmpty(params)) {
fetchQueueDetailsHelper(getState, params, dispatch);
}
};
if (params && !_.isEmpty(params)) {
fetchQueueDetailsHelper(getState, params, dispatch);
}
},
...createServerSideCollectionHandlers(

View file

@ -87,7 +87,7 @@ const config = {
slicer,
serialize,
merge,
key: 'sonarr'
key: 'lidarr'
};
export default persistState(paths, config);

View file

@ -25,7 +25,7 @@ export default function sentryMiddleware() {
version,
release,
isProduction
} = window.Sonarr;
} = window.Lidarr;
if (!analytics) {
return;

View file

@ -1,3 +1,3 @@
export default function getPathWithUrlBase(path) {
return `${window.Sonarr.urlBase}${path}`;
return `${window.Lidarr.urlBase}${path}`;
}

View file

@ -64,6 +64,7 @@ function CutoffUnmetRow(props) {
albumEntity={albumEntities.WANTED_CUTOFF_UNMET}
albumTitle={title}
showOpenArtistButton={true}
showOpenAlbumButton={true}
/>
</TableRowCell>
);

View file

@ -81,6 +81,7 @@ function MissingRow(props) {
albumEntity={albumEntities.WANTED_MISSING}
albumTitle={title}
showOpenArtistButton={true}
showOpenAlbumButton={true}
/>
</TableRowCell>
);

View file

@ -53,7 +53,7 @@
</body>
<script type="text/javascript">
window.Sonarr = {
window.Lidarr = {
apiRoot: 'API_ROOT',
apiKey: 'API_KEY',
release: 'APP_RELEASE',

View file

@ -1,8 +1,8 @@
import $ from 'jquery';
const absUrlRegex = /^(https?:)?\/\//i;
const apiRoot = window.Sonarr.apiRoot;
const urlBase = window.Sonarr.urlBase;
const apiRoot = window.Lidarr.apiRoot;
const urlBase = window.Lidarr.urlBase;
function isRelative(xhr) {
return !absUrlRegex.test(xhr.url);
@ -31,7 +31,7 @@ function addRootUrl(xhr) {
function addApiKey(xhr) {
xhr.headers = xhr.headers || {};
xhr.headers['X-Api-Key'] = window.Sonarr.apiKey;
xhr.headers['X-Api-Key'] = window.Lidarr.apiKey;
}
export default function() {

View file

@ -1,4 +1,4 @@
/* eslint no-undef: 0 */
import 'Shims/jquery';
__webpack_public_path__ = `${window.Sonarr.urlBase}/`;
__webpack_public_path__ = `${window.Lidarr.urlBase}/`;