Various UI Fixes and Updates

Closes #188
Closes #185
Closes #187
pull/190/head
Qstick 6 years ago
parent 3beac03c00
commit 54e9f88648

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

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

@ -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"
/>
}
</div>
}
{
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}
/>
</Label>
{
!!artistType &&
<Label size={sizes.LARGE}>
{artistType}
</Label>
}
{
!!albumCount &&
<Label size={sizes.LARGE}>
{albums}
</Label>
}
{
status === 'ended' &&
<Label
kind={kinds.DANGER}
size={sizes.LARGE}
>
Ended
</Label>
}
</div>
<div>
<Label size={sizes.LARGE}>
<HeartRating
rating={ratings.value}
iconSize={13}
/>
</Label>
{
!!artistType &&
<Label size={sizes.LARGE}>
{artistType}
</Label>
}
{
!!albumCount &&
<Label size={sizes.LARGE}>
{albums}
</Label>
}
{
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>
);
}
}

@ -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';

@ -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}`);
}
}
}

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

@ -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,

@ -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>
);
}

@ -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;

@ -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}

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

@ -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>

@ -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;

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

@ -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;
}
}

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

@ -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';

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

@ -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);

@ -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}`;

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

@ -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}
/>
}
{
!emptyParent && parent &&
<FileBrowserRow
type="parent"
name="..."
path={parent}
onPress={this.onRowPress}
/>
}
{
directories.map((directory) => {
return (
{
!!error &&
<div>Error loading contents</div>
}
{
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,

@ -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,

@ -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
};

@ -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}>

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

@ -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;
}
}

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

@ -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}>

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

@ -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
);
}
}

@ -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>

@ -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)}`));
}
};
}

@ -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}

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

@ -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,

@ -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);

@ -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;
}

@ -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
};

@ -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,

@ -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;
}

@ -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;

@ -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 />

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

@ -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);

@ -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}

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

@ -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 />

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

@ -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"

@ -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);

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

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

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

@ -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}

@ -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}

@ -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}

@ -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}

@ -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 });
}
}

@ -26,8 +26,8 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchQualityDefinitions,
saveQualityDefinitions
dispatchFetchQualityDefinitions: fetchQualityDefinitions,
dispatchSaveQualityDefinitions: saveQualityDefinitions
};
class QualityDefinitionsConnector extends Component {
@ -36,24 +36,34 @@ class QualityDefinitionsConnector extends Component {
// Lifecycle
componentDidMount() {
this.props.fetchQualityDefinitions();
}
this.props.dispatchFetchQualityDefinitions();
componentDidUpdate(prevProps) {
const {
hasPendingChanges
dispatchFetchQualityDefinitions,
dispatchSaveQualityDefinitions,
onChildMounted
} = this.props;
if (hasPendingChanges !== prevProps.hasPendingChanges) {
this.props.onHasPendingChange(hasPendingChanges);
}
dispatchFetchQualityDefinitions();
onChildMounted(dispatchSaveQualityDefinitions);
}
//
// Control
componentDidUpdate(prevProps) {
const {
hasPendingChanges,
isSaving,
onChildStateChange
} = this.props;
save = () => {
this.props.saveQualityDefinitions();
if (
prevProps.isSaving !== isSaving ||
prevProps.hasPendingChanges !== hasPendingChanges
) {
onChildStateChange({
isSaving,
hasPendingChanges
});
}
}
//
@ -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);

@ -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>

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

@ -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}
/>
{

@ -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];

@ -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
}));
});
}
},

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

@ -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(

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

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

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

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

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

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

@ -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() {

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

@ -33,87 +33,86 @@
"classnames": "2.2.5",
"clipboard": "1.7.1",
"create-react-class": "^15.6.2",
"css-loader": "0.28.7",
"css-loader": "0.28.9",
"del": "3.0.0",
"element-class": "0.2.2",
"esformatter": "0.10.0",
"eslint": "4.8.0",
"eslint": "4.16.0",
"eslint-loader": "1.9.0",
"eslint-plugin-filenames": "1.2.0",
"eslint-plugin-react": "7.4.0",
"eslint-plugin-react": "7.5.1",
"esprint": "0.4.0",
"extract-text-webpack-plugin": "3.0.1",
"file-loader": "1.1.5",
"filesize": "3.5.10",
"extract-text-webpack-plugin": "3.0.2",
"file-loader": "1.1.6",
"filesize": "3.5.11",
"gulp": "3.9.1",
"gulp-cached": "1.1.1",
"gulp-clean-css": "3.9.0",
"gulp-clean-css": "3.9.2",
"gulp-concat": "2.6.1",
"gulp-declare": "0.3.0",
"gulp-livereload": "3.8.1",
"gulp-postcss": "7.0.0",
"gulp-postcss": "7.0.1",
"gulp-print": "2.0.1",
"gulp-sourcemaps": "2.6.1",
"gulp-sourcemaps": "2.6.3",
"gulp-stripbom": "1.0.4",
"gulp-util": "3.0.8",
"gulp-watch": "4.3.11",
"gulp-watch": "5.0.0",
"gulp-wrap": "0.13.0",
"history": "4.7.2",
"jdu": "1.0.0",
"jquery": "3.2.1",
"jquery": "3.3.1",
"loader-utils": "^1.1.0",
"lodash": "4.17.4",
"mobile-detect": "1.3.7",
"moment": "2.18.1",
"mobile-detect": "1.4.1",
"moment": "2.20.1",
"mousetrap": "1.6.1",
"normalize.css": "7.0.0",
"postcss-loader": "2.0.6",
"postcss-mixins": "6.1.1",
"postcss-nested": "2.1.2",
"postcss-loader": "2.0.10",
"postcss-mixins": "6.2.0",
"postcss-nested": "3.0.0",
"postcss-simple-vars": "4.1.0",
"prop-types": "15.6.0",
"qs": "6.5.1",
"query-string": "5.0.0",
"query-string": "5.0.1",
"raven-for-redux": "1.0.0",
"raven-js": "3.17.0",
"react": "15.6.0",
"react": "16.2.0",
"react-addons-shallow-compare": "15.6.2",
"react-async-script": "0.9.1",
"react-autosuggest": "9.3.2",
"react-custom-scrollbars": "4.1.2",
"react-custom-scrollbars": "4.2.1",
"react-dnd": "2.5.4",
"react-dnd-html5-backend": "2.5.4",
"react-document-title": "2.0.3",
"react-dom": "15.6.0",
"react-google-recaptcha": "0.9.7",
"react-lazyload": "2.2.7",
"react-dom": "16.2.0",
"react-google-recaptcha": "0.9.9",
"react-lazyload": "2.3.0",
"react-measure": "1.4.7",
"react-portal": "3.1.0",
"react-redux": "5.0.6",
"react-router-dom": "4.2.2",
"react-router-redux": "5.0.0-alpha.6",
"react-router-redux": "5.0.0-alpha.9",
"react-slider": "0.9.0",
"react-tabs": "2.1.0",
"react-tag-autocomplete": "5.4.1",
"react-tether": "0.5.7",
"react-text-truncate": "0.12.0",
"react-virtualized": "9.10.1",
"react-tabs": "2.2.1",
"react-tag-autocomplete": "5.5.0",
"react-tether": "0.6.1",
"react-text-truncate": "0.12.1",
"react-virtualized": "9.18.0",
"redux": "3.7.2",
"redux-actions": "2.2.1",
"redux-batched-actions": "0.2.0",
"redux-batched-actions": "0.2.1",
"redux-localstorage": "0.4.1",
"redux-thunk": "2.2.0",
"require-nocache": "1.0.0",
"reselect": "3.0.1",
"run-sequence": "2.2.0",
"run-sequence": "2.2.1",
"signalr": "2.2.2",
"streamqueue": "1.1.1",
"style-loader": "0.19.0",
"stylelint": "8.2.0",
"stylelint-order": "0.7.0",
"tar.gz": "1.0.5",
"streamqueue": "1.1.2",
"style-loader": "0.19.1",
"stylelint": "8.4.0",
"stylelint-order": "0.8.0",
"tar.gz": "1.0.7",
"url-loader": "0.6.2",
"webpack": "3.6.0",
"webpack": "3.10.0",
"webpack-stream": "^4.0.0"
},
"main": "index.js"

@ -1,6 +1,7 @@
using System;
using System;
using System.Linq;
using Nancy.Responses;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Messaging.Events;
@ -16,12 +17,18 @@ namespace Lidarr.Api.V1.Queue
{
private readonly IQueueService _queueService;
private readonly IPendingReleaseService _pendingReleaseService;
private readonly Debouncer _broadcastDebounce;
public QueueStatusModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService)
: base(broadcastSignalRMessage, "queue/status")
{
_queueService = queueService;
_pendingReleaseService = pendingReleaseService;
_broadcastDebounce = new Debouncer(BroadcastChange, TimeSpan.FromSeconds(5));
Get["/"] = x => GetQueueStatusResponse();
}
@ -32,25 +39,38 @@ namespace Lidarr.Api.V1.Queue
private QueueStatusResource GetQueueStatus()
{
_broadcastDebounce.Pause();
var queue = _queueService.GetQueue();
var pending = _pendingReleaseService.GetPendingQueue();
return new QueueStatusResource
var resource = new QueueStatusResource
{
Count = queue.Count + pending.Count,
Errors = queue.Any(q => q.TrackedDownloadStatus.Equals("Error", StringComparison.InvariantCultureIgnoreCase)),
Warnings = queue.Any(q => q.TrackedDownloadStatus.Equals("Warning", StringComparison.InvariantCultureIgnoreCase))
};
_broadcastDebounce.Resume();
return resource;
}
public void Handle(QueueUpdatedEvent message)
private void BroadcastChange()
{
BroadcastResourceChange(ModelAction.Updated, GetQueueStatus());
}
public void Handle(QueueUpdatedEvent message)
{
_broadcastDebounce.Execute();
}
public void Handle(PendingReleasesUpdatedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, GetQueueStatus());
_broadcastDebounce.Execute();
}
}
}
}

@ -17,8 +17,9 @@ using HttpStatusCode = System.Net.HttpStatusCode;
namespace Lidarr.Api.V1.TrackFiles
{
public class TrackModule : LidarrRestModuleWithSignalR<TrackFileResource, TrackFile>,
IHandle<TrackFileAddedEvent>
public class TrackFileModule : LidarrRestModuleWithSignalR<TrackFileResource, TrackFile>,
IHandle<TrackFileAddedEvent>,
IHandle<TrackFileDeletedEvent>
{
private readonly IMediaFileService _mediaFileService;
private readonly IDeleteMediaFiles _mediaFileDeletionService;
@ -26,7 +27,7 @@ namespace Lidarr.Api.V1.TrackFiles
private readonly IAlbumService _albumService;
private readonly IUpgradableSpecification _upgradableSpecification;
public TrackModule(IBroadcastSignalRMessage signalRBroadcaster,
public TrackFileModule(IBroadcastSignalRMessage signalRBroadcaster,
IMediaFileService mediaFileService,
IDeleteMediaFiles mediaFileDeletionService,
IArtistService artistService,
@ -170,5 +171,11 @@ namespace Lidarr.Api.V1.TrackFiles
{
BroadcastResourceChange(ModelAction.Updated, message.TrackFile.Id);
}
public void Handle(TrackFileDeletedEvent message)
{
BroadcastResourceChange(ModelAction.Deleted, message.TrackFile.Id);
}
}
}

@ -2,6 +2,7 @@ using System.Collections.Generic;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Events;
@ -15,7 +16,8 @@ namespace Lidarr.Api.V1.Tracks
{
public abstract class TrackModuleWithSignalR : LidarrRestModuleWithSignalR<TrackResource, Track>,
IHandle<TrackInfoRefreshedEvent>,
IHandle<TrackImportedEvent>
IHandle<TrackImportedEvent>,
IHandle<TrackFileDeletedEvent>
{
protected readonly ITrackService _trackService;
protected readonly IArtistService _artistService;
@ -131,5 +133,13 @@ namespace Lidarr.Api.V1.Tracks
}
}
public void Handle(TrackFileDeletedEvent message)
{
foreach (var track in message.TrackFile.Tracks.Value)
{
BroadcastResourceChange(ModelAction.Deleted, track.Id);
}
}
}
}

@ -35,8 +35,15 @@ namespace Lidarr.Http
protected void BroadcastResourceChange(ModelAction action, int id)
{
var resource = GetResourceById(id);
BroadcastResourceChange(action, resource);
if (action == ModelAction.Deleted)
{
BroadcastResourceChange(action, new TResource { Id = id });
}
else
{
var resource = GetResourceById(id);
BroadcastResourceChange(action, resource);
}
}

@ -83,6 +83,10 @@ namespace NzbDrone.Core.Test.MediaFiles
.Returns(new TrackFileMoveResult());
_downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesWithRelativePath(It.IsAny<int>(), It.IsAny<string>()))
.Returns(new List<TrackFile>());
}
[Test]
@ -198,5 +202,19 @@ namespace NzbDrone.Core.Test.MediaFiles
Mocker.GetMock<IUpgradeMediaFiles>()
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().LocalTrack, false), Times.Once());
}
[Test]
public void should_delete_existing_metadata_files_with_the_same_path()
{
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesWithRelativePath(It.IsAny<int>(), It.IsAny<string>()))
.Returns(Builder<TrackFile>.CreateListOfSize(1).BuildList());
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, false);
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Delete(It.IsAny<TrackFile>(), DeleteMediaFileReason.ManualOverride), Times.Once());
}
}
}

@ -26,7 +26,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
var monoVersion = _platformInfo.Version;
if (monoVersion == new Version("4.4.0") || monoVersion == new Version("4.4.1"))
if (monoVersion == new Version("4.4") || monoVersion == new Version("4.4.1"))
{
_logger.Debug("Mono version {0}", monoVersion);
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Your Mono version {monoVersion} has a bug that causes issues connecting to indexers/download clients. You should upgrade to a higher version");
@ -34,7 +34,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (monoVersion >= new Version("4.4"))
{
_logger.Debug("Mono version is 4.6 or better: {0}", monoVersion);
_logger.Debug("Mono version is 4.4 or better: {0}", monoVersion);
return new HealthCheck(GetType());
}

@ -248,6 +248,12 @@ namespace NzbDrone.Core.History
_logger.Debug("Removing track file from DB as part of cleanup routine, not creating history event.");
return;
}
else if (message.Reason == DeleteMediaFileReason.ManualOverride)
{
_logger.Debug("Removing track file from DB as part of manual override of existing file, not creating history event.");
return;
}
foreach (var track in message.TrackFile.Tracks.Value)
{

@ -1,10 +1,11 @@
namespace NzbDrone.Core.MediaFiles
namespace NzbDrone.Core.MediaFiles
{
public enum DeleteMediaFileReason
{
MissingFromDisk,
Manual,
Upgrade,
NoLinkedEpisodes
NoLinkedEpisodes,
ManualOverride
}
}

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
@ -11,6 +11,7 @@ namespace NzbDrone.Core.MediaFiles
List<TrackFile> GetFilesByArtist(int artistId);
List<TrackFile> GetFilesByAlbum(int albumId);
List<TrackFile> GetFilesWithoutMediaInfo();
List<TrackFile> GetFilesWithRelativePath(int artistId, string relativePath);
}
@ -35,5 +36,13 @@ namespace NzbDrone.Core.MediaFiles
{
return Query.Where(c => c.AlbumId == albumId).ToList();
}
public List<TrackFile> GetFilesWithRelativePath(int artistId, string relativePath)
{
return Query.Where(c => c.ArtistId == artistId)
.AndWhere(c => c.RelativePath == relativePath)
.ToList();
}
}
}
}

@ -24,6 +24,7 @@ namespace NzbDrone.Core.MediaFiles
List<string> FilterExistingFiles(List<string> files, Artist artist);
TrackFile Get(int id);
List<TrackFile> Get(IEnumerable<int> ids);
List<TrackFile> GetFilesWithRelativePath(int artistId, string relativePath);
}
@ -98,6 +99,11 @@ namespace NzbDrone.Core.MediaFiles
return _mediaFileRepository.Get(ids).ToList();
}
public List<TrackFile> GetFilesWithRelativePath(int artistId, string relativePath)
{
return _mediaFileRepository.GetFilesWithRelativePath(artistId, relativePath);
}
public void HandleAsync(ArtistDeletedEvent message)
{
var files = GetFilesByArtist(message.Artist.Id);

@ -147,7 +147,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
}
Logger.Debug()
.Message("Unknown audio format: '{0}' in '{1}'.", string.Join(", ", audioFormat, audioCodecID, audioProfile, audioCodecLibrary))
.Message("Unknown audio format: '{0}'.", string.Join(", ", audioFormat, audioCodecID, audioProfile, audioCodecLibrary))
.WriteSentryWarn("UnknownAudioFormat", mediaInfo.ContainerFormat, audioFormat, audioCodecID)
.Write();

@ -115,6 +115,15 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
else
{
trackFile.RelativePath = localTrack.Artist.Path.GetRelativePath(trackFile.Path);
// Delete existing files from the DB mapped to this path
var previousFiles = _mediaFileService.GetFilesWithRelativePath(localTrack.Artist.Id, trackFile.RelativePath);
foreach (var previousFile in previousFiles)
{
_mediaFileService.Delete(previousFile, DeleteMediaFileReason.ManualOverride);
}
}
_mediaFileService.Add(trackFile);

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save