diff --git a/.all-contributorsrc b/.all-contributorsrc index c0f24e34..7902f4aa 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -458,6 +458,15 @@ "contributions": [ "translation" ] + }, + { + "login": "Dabu-dot", + "name": "Dabu-dot", + "avatar_url": "https://avatars.githubusercontent.com/u/52525576?v=4", + "profile": "https://github.com/Dabu-dot", + "contributions": [ + "translation" + ] } ], "badgeTemplate": "\"All-orange.svg\"/>", diff --git a/README.md b/README.md index 2fa70358..3bd9f66e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Language grade: JavaScript GitHub -All Contributors +All Contributors

@@ -137,6 +137,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Jono Cairns

đź’»
DJScias

🌍 + +
Dabu-dot

🌍 + diff --git a/docs/using-overseerr/settings/README.md b/docs/using-overseerr/settings/README.md index 1753c944..bdf9cd34 100644 --- a/docs/using-overseerr/settings/README.md +++ b/docs/using-overseerr/settings/README.md @@ -178,11 +178,11 @@ If the hostname or IP address you configured above is not accessible outside you #### Enable Scan -Tick this box if you would like to scan your Radarr/Sonarr server for existing media/request status. It is recommended that you enable this setting, so that users cannot submit requests for media which has already been requested or is already available. +Enable this setting if you would like to scan your Radarr/Sonarr server for existing media/request status. It is recommended that you enable this setting, so that users cannot submit requests for media which has already been requested or is already available. -#### Disable Auto-Search +#### Enable Automatic Search -If you do not want Radarr/Sonarr to automatically search for media upon submission of a request, you can disable this setting. +Enable this setting to have Radarr/Sonarr to automatically search for media upon approval of a request. ## Notifications diff --git a/package.json b/package.json index 08f8a302..6092bdde 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "license": "MIT", "dependencies": { "@headlessui/react": "^1.0.0", + "@heroicons/react": "^1.0.1", "@supercharge/request-ip": "^1.1.2", "@svgr/webpack": "^5.5.0", "@tanem/react-nprogress": "^3.0.62", diff --git a/server/api/servarr/radarr.ts b/server/api/servarr/radarr.ts index 59407720..0e0a41f1 100644 --- a/server/api/servarr/radarr.ts +++ b/server/api/servarr/radarr.ts @@ -72,7 +72,8 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { } catch (e) { logger.error('Error retrieving movie by TMDb ID', { label: 'Radarr API', - message: e.message, + errorMessage: e.message, + tmdbId: id, }); throw new Error('Movie not found'); } @@ -89,12 +90,13 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { 'Title already exists and is available. Skipping add and returning success', { label: 'Radarr', + movie, } ); return movie; } - // movie exists in radarr but is neither downloaded nor monitored + // movie exists in Radarr but is neither downloaded nor monitored if (movie.id && !movie.monitored) { const response = await this.axios.put(`/movie`, { ...movie, @@ -115,16 +117,25 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { if (response.data.monitored) { logger.info( - 'Found existing title in Radarr and set it to monitored. Returning success', - { label: 'Radarr' } + 'Found existing title in Radarr and set it to monitored.', + { + label: 'Radarr', + movieId: response.data.id, + movieTitle: response.data.title, + } ); logger.debug('Radarr update details', { label: 'Radarr', movie: response.data, }); + + if (options.searchNow) { + this.searchMovie(response.data.id); + } + return response.data; } else { - logger.error('Failed to update existing movie in Radarr', { + logger.error('Failed to update existing movie in Radarr.', { label: 'Radarr', options, }); @@ -183,6 +194,26 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { throw new Error('Failed to add movie to Radarr'); } }; + + public async searchMovie(movieId: number): Promise { + logger.info('Executing movie search command', { + label: 'Radarr API', + movieId, + }); + + try { + await this.runCommand('MoviesSearch', { movieIds: [movieId] }); + } catch (e) { + logger.error( + 'Something went wrong while executing Radarr movie search.', + { + label: 'Radarr API', + errorMessage: e.message, + movieId, + } + ); + } + } } export default RadarrAPI; diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts index 12337839..b6793ed3 100644 --- a/server/api/servarr/sonarr.ts +++ b/server/api/servarr/sonarr.ts @@ -113,7 +113,8 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> { } catch (e) { logger.error('Error retrieving series by series title', { label: 'Sonarr API', - message: e.message, + errorMessage: e.message, + title, }); throw new Error('No series found'); } @@ -135,7 +136,8 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> { } catch (e) { logger.error('Error retrieving series by tvdb ID', { label: 'Sonarr API', - message: e.message, + errorMessage: e.message, + tvdbId: id, }); throw new Error('Series not found'); } @@ -156,16 +158,21 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> { ); if (newSeriesResponse.data.id) { - logger.info('Sonarr accepted request. Updated existing series', { + logger.info('Updated existing series in Sonarr.', { label: 'Sonarr', + seriesId: newSeriesResponse.data.id, + seriesTitle: newSeriesResponse.data.title, }); logger.debug('Sonarr update details', { label: 'Sonarr', movie: newSeriesResponse.data, }); + if (options.searchNow) { this.searchSeries(newSeriesResponse.data.id); } + + return newSeriesResponse.data; } else { logger.error('Failed to update series in Sonarr', { label: 'Sonarr', @@ -173,8 +180,6 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> { }); throw new Error('Failed to update series in Sonarr'); } - - return newSeriesResponse.data; } const createdSeriesResponse = await this.axios.post( @@ -223,7 +228,7 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> { logger.error('Something went wrong while adding a series to Sonarr.', { label: 'Sonarr API', errorMessage: e.message, - error: e, + options, response: e?.response?.data, }); throw new Error('Failed to add series'); @@ -244,7 +249,7 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> { 'Something went wrong while retrieving Sonarr language profiles.', { label: 'Sonarr API', - message: e.message, + errorMessage: e.message, } ); @@ -253,11 +258,23 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> { } public async searchSeries(seriesId: number): Promise { - logger.info('Executing series search command', { + logger.info('Executing series search command.', { label: 'Sonarr API', seriesId, }); - await this.runCommand('SeriesSearch', { seriesId }); + + try { + await this.runCommand('SeriesSearch', { seriesId }); + } catch (e) { + logger.error( + 'Something went wrong while executing Sonarr series search.', + { + label: 'Sonarr API', + errorMessage: e.message, + seriesId, + } + ); + } } private buildSeasonList( diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index 167d1db0..be28e35d 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -142,7 +142,9 @@ export class MediaRequest { if (this.type === MediaType.MOVIE) { const movie = await tmdb.getMovie({ movieId: media.tmdbId }); notificationManager.sendNotification(Notification.MEDIA_PENDING, { - subject: movie.title, + subject: `${movie.title}${ + movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' + }`, message: movie.overview, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, media, @@ -153,7 +155,9 @@ export class MediaRequest { if (this.type === MediaType.TV) { const tv = await tmdb.getTvShow({ tvId: media.tmdbId }); notificationManager.sendNotification(Notification.MEDIA_PENDING, { - subject: tv.name, + subject: `${tv.name}${ + tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : '' + }`, message: tv.overview, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, media, @@ -210,7 +214,9 @@ export class MediaRequest { : Notification.MEDIA_APPROVED : Notification.MEDIA_DECLINED, { - subject: movie.title, + subject: `${movie.title}${ + movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' + }`, message: movie.overview, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, notifyUser: autoApproved ? undefined : this.requestedBy, @@ -227,7 +233,9 @@ export class MediaRequest { : Notification.MEDIA_APPROVED : Notification.MEDIA_DECLINED, { - subject: tv.name, + subject: `${tv.name}${ + tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : '' + }`, message: tv.overview, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, notifyUser: autoApproved ? undefined : this.requestedBy, @@ -492,7 +500,9 @@ export class MediaRequest { ); notificationManager.sendNotification(Notification.MEDIA_FAILED, { - subject: movie.title, + subject: `${movie.title}${ + movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' + }`, message: movie.overview, media, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, @@ -700,7 +710,11 @@ export class MediaRequest { ); notificationManager.sendNotification(Notification.MEDIA_FAILED, { - subject: series.name, + subject: `${series.name}${ + series.first_air_date + ? ` (${series.first_air_date.slice(0, 4)})` + : '' + }`, message: series.overview, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${series.poster_path}`, media, diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index f1f237f5..70af56ba 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -1,5 +1,4 @@ import logger from '../../logger'; -import { getSettings } from '../settings'; import type { NotificationAgent, NotificationPayload } from './agents/agent'; export enum Notification { @@ -45,13 +44,13 @@ class NotificationManager { type: Notification, payload: NotificationPayload ): void { - const settings = getSettings().notifications; logger.info(`Sending notification(s) for ${Notification[type]}`, { label: 'Notifications', subject: payload.subject, }); + this.activeAgents.forEach((agent) => { - if (settings.enabled && agent.shouldSend(type)) { + if (agent.shouldSend(type)) { agent.send(type, payload); } }); diff --git a/server/lib/settings.ts b/server/lib/settings.ts index bb82c7ef..290d4040 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -179,7 +179,6 @@ interface NotificationAgents { } interface NotificationSettings { - enabled: boolean; agents: NotificationAgents; } @@ -234,7 +233,6 @@ class Settings { initialized: false, }, notifications: { - enabled: true, agents: { email: { enabled: false, diff --git a/server/routes/request.ts b/server/routes/request.ts index 6ad4ac05..df0b5545 100644 --- a/server/routes/request.ts +++ b/server/routes/request.ts @@ -175,6 +175,36 @@ requestRoutes.post( }); } + if (req.body.is4k) { + if ( + req.body.mediaType === MediaType.MOVIE && + !req.user?.hasPermission( + [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE], + { + type: 'or', + } + ) + ) { + return next({ + status: 403, + message: 'You do not have permission to make 4K movie requests.', + }); + } else if ( + req.body.mediaType === MediaType.TV && + !req.user?.hasPermission( + [Permission.REQUEST_4K, Permission.REQUEST_4K_TV], + { + type: 'or', + } + ) + ) { + return next({ + status: 403, + message: 'You do not have permission to make 4K series requests.', + }); + } + } + const quotas = await requestUser.getQuota(); if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) { @@ -463,7 +493,6 @@ requestRoutes.get('/:requestId', async (req, res, next) => { requestRoutes.put<{ requestId: string }>( '/:requestId', - isAuthenticated(Permission.MANAGE_REQUESTS), async (req, res, next) => { const requestRepository = getRepository(MediaRequest); const userRepository = getRepository(User); @@ -473,17 +502,30 @@ requestRoutes.put<{ requestId: string }>( ); if (!request) { - return next({ status: 404, message: 'Request not found' }); + return next({ status: 404, message: 'Request not found.' }); + } + + if ( + (request.requestedBy.id !== req.user?.id || + (req.body.mediaType !== 'tv' && + !req.user?.hasPermission(Permission.REQUEST_ADVANCED))) && + !req.user?.hasPermission(Permission.MANAGE_REQUESTS) + ) { + return next({ + status: 403, + message: 'You do not have permission to modify this request.', + }); } let requestUser = req.user; if ( req.body.userId && - !( - req.user?.hasPermission(Permission.MANAGE_USERS) && - req.user?.hasPermission(Permission.MANAGE_REQUESTS) - ) + req.body.userId !== req.user?.id && + !req.user?.hasPermission([ + Permission.MANAGE_USERS, + Permission.MANAGE_REQUESTS, + ]) ) { return next({ status: 403, @@ -516,7 +558,7 @@ requestRoutes.put<{ requestId: string }>( if (!requestedSeasons || requestedSeasons.length === 0) { throw new Error( - 'Missing seasons. If you want to cancel a tv request, use the DELETE method.' + 'Missing seasons. If you want to cancel a series request, use the DELETE method.' ); } @@ -603,7 +645,7 @@ requestRoutes.delete('/:requestId', async (req, res, next) => { ) { return next({ status: 401, - message: 'You do not have permission to remove this request', + message: 'You do not have permission to delete this request.', }); } @@ -612,7 +654,7 @@ requestRoutes.delete('/:requestId', async (req, res, next) => { return res.status(204).send(); } catch (e) { logger.error(e.message); - next({ status: 404, message: 'Request not found' }); + next({ status: 404, message: 'Request not found.' }); } }); @@ -638,7 +680,7 @@ requestRoutes.post<{ label: 'Media Request', message: e.message, }); - next({ status: 404, message: 'Request not found' }); + next({ status: 404, message: 'Request not found.' }); } } ); @@ -682,7 +724,7 @@ requestRoutes.post<{ label: 'Media Request', message: e.message, }); - next({ status: 404, message: 'Request not found' }); + next({ status: 404, message: 'Request not found.' }); } } ); diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index 719e8c9f..514aa1de 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -4,11 +4,13 @@ import fs from 'fs'; import { merge, omit } from 'lodash'; import path from 'path'; import { getRepository } from 'typeorm'; +import { URL } from 'url'; import PlexAPI from '../../api/plexapi'; import PlexTvAPI from '../../api/plextv'; import Media from '../../entity/Media'; import { MediaRequest } from '../../entity/MediaRequest'; import { User } from '../../entity/User'; +import { PlexConnection } from '../../interfaces/api/plexInterfaces'; import { LogMessage, LogsResultsResponse, @@ -129,13 +131,32 @@ settingsRoutes.get('/plex/devices/servers', async (req, res, next) => { if (devices) { await Promise.all( devices.map(async (device) => { + const plexDirectConnections: PlexConnection[] = []; + + device.connection.forEach((connection) => { + const url = new URL(connection.uri); + + if (url.hostname !== connection.address) { + const plexDirectConnection = { ...connection }; + plexDirectConnection.address = url.hostname; + plexDirectConnections.push(plexDirectConnection); + + // Connect to IP addresses over HTTP + connection.protocol = 'http'; + } + }); + + plexDirectConnections.forEach((plexDirectConnection) => { + device.connection.push(plexDirectConnection); + }); + await Promise.all( device.connection.map(async (connection) => { const plexDeviceSettings = { ...settings.plex, ip: connection.address, port: connection.port, - useSsl: !connection.local && connection.protocol === 'https', + useSsl: connection.protocol === 'https', }; const plexClient = new PlexAPI({ plexToken: admin.plexToken, @@ -149,7 +170,7 @@ settingsRoutes.get('/plex/devices/servers', async (req, res, next) => { connection.message = 'OK'; } catch (e) { connection.status = 500; - connection.message = e.message; + connection.message = e.message.split(':')[0]; } }) ); diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index 0c904691..a0dab71c 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -81,10 +81,25 @@ router.post( const body = req.body; const userRepository = getRepository(User); + const existingUser = await userRepository.findOne({ + where: { email: body.email }, + }); + + if (existingUser) { + return next({ + status: 409, + message: 'User already exists with submitted email.', + errors: ['USER_EXISTS'], + }); + } + const passedExplicitPassword = body.password && body.password.length > 0; const avatar = gravatarUrl(body.email, { default: 'mm', size: 200 }); - if (!passedExplicitPassword && !settings.notifications.agents.email) { + if ( + !passedExplicitPassword && + !settings.notifications.agents.email.enabled + ) { throw new Error('Email notifications must be enabled'); } diff --git a/src/assets/available.svg b/src/assets/available.svg deleted file mode 100644 index 87b9bdeb..00000000 --- a/src/assets/available.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/bolt.svg b/src/assets/bolt.svg deleted file mode 100644 index d83a0d8a..00000000 --- a/src/assets/bolt.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/download.svg b/src/assets/download.svg deleted file mode 100644 index 4dd0492b..00000000 --- a/src/assets/download.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/requested.svg b/src/assets/requested.svg deleted file mode 100644 index 825678d0..00000000 --- a/src/assets/requested.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/unavailable.svg b/src/assets/unavailable.svg deleted file mode 100644 index d9474805..00000000 --- a/src/assets/unavailable.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/useradd.svg b/src/assets/useradd.svg deleted file mode 100644 index 1c6055ec..00000000 --- a/src/assets/useradd.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/xcircle.svg b/src/assets/xcircle.svg deleted file mode 100644 index 7a7b4533..00000000 --- a/src/assets/xcircle.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/CollectionDetails/index.tsx b/src/components/CollectionDetails/index.tsx index 93447749..ea4f52b3 100644 --- a/src/components/CollectionDetails/index.tsx +++ b/src/components/CollectionDetails/index.tsx @@ -1,3 +1,4 @@ +import { DownloadIcon, DuplicateIcon } from '@heroicons/react/outline'; import axios from 'axios'; import { uniq } from 'lodash'; import Link from 'next/link'; @@ -248,22 +249,7 @@ const CollectionDetails: React.FC = ({ title={intl.formatMessage( is4k ? messages.requestcollection4k : messages.requestcollection )} - iconSvg={ - - - - } + iconSvg={} >

{intl.formatMessage( @@ -355,20 +341,7 @@ const CollectionDetails: React.FC = ({ }} text={ <> - - - + {intl.formatMessage( hasRequestable @@ -393,20 +366,7 @@ const CollectionDetails: React.FC = ({ setIs4k(true); }} > - - - + {intl.formatMessage(messages.requestcollection4k)} diff --git a/src/components/Common/Alert/index.tsx b/src/components/Common/Alert/index.tsx index b29513cc..9f62131a 100644 --- a/src/components/Common/Alert/index.tsx +++ b/src/components/Common/Alert/index.tsx @@ -1,3 +1,8 @@ +import { + ExclamationIcon, + InformationCircleIcon, + XCircleIcon, +} from '@heroicons/react/solid'; import React from 'react'; interface AlertProps { @@ -10,21 +15,7 @@ const Alert: React.FC = ({ title, children, type }) => { bgColor: 'bg-yellow-600', titleColor: 'text-yellow-200', textColor: 'text-yellow-300', - svg: ( - - ), + svg: , }; switch (type) { @@ -33,22 +24,7 @@ const Alert: React.FC = ({ title, children, type }) => { bgColor: 'bg-indigo-600', titleColor: 'text-indigo-200', textColor: 'text-indigo-300', - svg: ( - - - - ), + svg: , }; break; case 'error': @@ -56,22 +32,7 @@ const Alert: React.FC = ({ title, children, type }) => { bgColor: 'bg-red-600', titleColor: 'text-red-200', textColor: 'text-red-300', - svg: ( - - - - ), + svg: , }; break; } diff --git a/src/components/Common/ButtonWithDropdown/index.tsx b/src/components/Common/ButtonWithDropdown/index.tsx index d429e114..65a8c741 100644 --- a/src/components/Common/ButtonWithDropdown/index.tsx +++ b/src/components/Common/ButtonWithDropdown/index.tsx @@ -1,13 +1,14 @@ +import { ChevronDownIcon } from '@heroicons/react/solid'; import React, { - useState, - useRef, AnchorHTMLAttributes, - ReactNode, ButtonHTMLAttributes, + ReactNode, + useRef, + useState, } from 'react'; import useClickOutside from '../../../hooks/useClickOutside'; -import Transition from '../../Transition'; import { withProperties } from '../../../utils/typeHelpers'; +import Transition from '../../Transition'; interface DropdownItemProps extends AnchorHTMLAttributes { buttonType?: 'primary' | 'ghost'; @@ -102,18 +103,7 @@ const ButtonWithDropdown: React.FC = ({ {dropdownIcon ? ( dropdownIcon ) : ( - - - + )} = ({ links }) => { @@ -20,27 +21,8 @@ const PlayButton: React.FC = ({ links }) => { buttonType="ghost" text={ <> - - - - - {links[0].text} + {links[0].svg} + {links[0].text} } onClick={() => { @@ -57,6 +39,7 @@ const PlayButton: React.FC = ({ links }) => { }} buttonType="ghost" > + {link.svg} {link.text} ); diff --git a/src/components/Common/SlideOver/index.tsx b/src/components/Common/SlideOver/index.tsx index 79a9a0bb..4dac751a 100644 --- a/src/components/Common/SlideOver/index.tsx +++ b/src/components/Common/SlideOver/index.tsx @@ -1,4 +1,5 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ +import { XIcon } from '@heroicons/react/outline'; import React, { useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import { useLockBodyScroll } from '../../../hooks/useLockBodyScroll'; @@ -81,20 +82,7 @@ const SlideOver: React.FC = ({ className="text-indigo-200 transition duration-150 ease-in-out hover:text-white" onClick={() => onClose()} > - - - + diff --git a/src/components/Discover/MovieGenreSlider/index.tsx b/src/components/Discover/MovieGenreSlider/index.tsx index 6ebbed11..56abf7d9 100644 --- a/src/components/Discover/MovieGenreSlider/index.tsx +++ b/src/components/Discover/MovieGenreSlider/index.tsx @@ -1,12 +1,13 @@ +import { ArrowCircleRightIcon } from '@heroicons/react/outline'; +import Link from 'next/link'; import React, { useContext } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import GenreCard from '../../GenreCard'; -import Slider from '../../Slider'; import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces'; import { LanguageContext } from '../../../context/LanguageContext'; +import GenreCard from '../../GenreCard'; +import Slider from '../../Slider'; import { genreColorMap } from '../constants'; -import Link from 'next/link'; const messages = defineMessages({ moviegenres: 'Movie Genres', @@ -29,20 +30,7 @@ const MovieGenreSlider: React.FC = () => { {intl.formatMessage(messages.moviegenres)} - - - + diff --git a/src/components/Discover/TvGenreSlider/index.tsx b/src/components/Discover/TvGenreSlider/index.tsx index 20b1b1e2..37f1ee18 100644 --- a/src/components/Discover/TvGenreSlider/index.tsx +++ b/src/components/Discover/TvGenreSlider/index.tsx @@ -1,12 +1,13 @@ +import { ArrowCircleRightIcon } from '@heroicons/react/outline'; +import Link from 'next/link'; import React, { useContext } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import GenreCard from '../../GenreCard'; -import Slider from '../../Slider'; import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces'; import { LanguageContext } from '../../../context/LanguageContext'; +import GenreCard from '../../GenreCard'; +import Slider from '../../Slider'; import { genreColorMap } from '../constants'; -import Link from 'next/link'; const messages = defineMessages({ tvgenres: 'Series Genres', @@ -29,20 +30,7 @@ const TvGenreSlider: React.FC = () => { {intl.formatMessage(messages.tvgenres)} - - - + diff --git a/src/components/Discover/index.tsx b/src/components/Discover/index.tsx index 8560a535..bb80d08b 100644 --- a/src/components/Discover/index.tsx +++ b/src/components/Discover/index.tsx @@ -1,3 +1,4 @@ +import { ArrowCircleRightIcon } from '@heroicons/react/outline'; import Link from 'next/link'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -66,20 +67,7 @@ const Discover: React.FC = () => { {intl.formatMessage(messages.recentrequests)} - - - + diff --git a/src/components/Layout/LanguagePicker/index.tsx b/src/components/Layout/LanguagePicker/index.tsx index 25111f30..683fe5f4 100644 --- a/src/components/Layout/LanguagePicker/index.tsx +++ b/src/components/Layout/LanguagePicker/index.tsx @@ -1,3 +1,4 @@ +import { TranslateIcon } from '@heroicons/react/solid'; import React, { useContext, useRef, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { @@ -100,18 +101,7 @@ const LanguagePicker: React.FC = () => { aria-label="Language Picker" onClick={() => setDropdownOpen(true)} > - - - + { @@ -6,19 +7,7 @@ const Notifications: React.FC = () => { className="p-1 text-gray-400 rounded-full hover:bg-gray-500 hover:text-white focus:outline-none focus:ring focus:text-white" aria-label="Notifications" > - - - + ); }; diff --git a/src/components/Layout/SearchInput/index.tsx b/src/components/Layout/SearchInput/index.tsx index 9d0aa634..9042ef45 100644 --- a/src/components/Layout/SearchInput/index.tsx +++ b/src/components/Layout/SearchInput/index.tsx @@ -1,7 +1,8 @@ +import { XCircleIcon } from '@heroicons/react/outline'; +import { SearchIcon } from '@heroicons/react/solid'; import React from 'react'; -import useSearchInput from '../../../hooks/useSearchInput'; import { defineMessages, useIntl } from 'react-intl'; -import ClearButton from '../../../assets/xcircle.svg'; +import useSearchInput from '../../../hooks/useSearchInput'; const messages = defineMessages({ searchPlaceholder: 'Search Movies & TV', @@ -18,13 +19,7 @@ const SearchInput: React.FC = () => {

- - - +
{ className="absolute inset-y-0 p-1 m-auto text-gray-400 transition border-none outline-none right-2 h-7 w-7 focus:outline-none focus:border-none hover:text-white" onClick={() => clear()} > - + )}
diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx index 7558b95b..dbd15625 100644 --- a/src/components/Layout/Sidebar/index.tsx +++ b/src/components/Layout/Sidebar/index.tsx @@ -1,3 +1,10 @@ +import { + ClockIcon, + CogIcon, + SparklesIcon, + XIcon, +} from '@heroicons/react/outline'; +import { UsersIcon } from '@heroicons/react/solid'; import Link from 'next/link'; import { useRouter } from 'next/router'; import React, { ReactNode, useRef } from 'react'; @@ -33,20 +40,7 @@ const SidebarLinks: SidebarLinkProps[] = [ href: '/', messagesKey: 'dashboard', svgIcon: ( - - - + ), activeRegExp: /^\/(discover\/?(movies|tv)?)?$/, }, @@ -54,20 +48,7 @@ const SidebarLinks: SidebarLinkProps[] = [ href: '/requests', messagesKey: 'requests', svgIcon: ( - - - + ), activeRegExp: /^\/requests/, }, @@ -75,14 +56,7 @@ const SidebarLinks: SidebarLinkProps[] = [ href: '/users', messagesKey: 'users', svgIcon: ( - - - + ), activeRegExp: /^\/users/, requiredPermission: Permission.MANAGE_USERS, @@ -91,26 +65,7 @@ const SidebarLinks: SidebarLinkProps[] = [ href: '/settings', messagesKey: 'settings', svgIcon: ( - - - - + ), activeRegExp: /^\/settings/, requiredPermission: Permission.MANAGE_SETTINGS, @@ -157,19 +112,7 @@ const Sidebar: React.FC = ({ open, setClosed }) => { aria-label="Close sidebar" onClick={() => setClosed()} > - - - + diff --git a/src/components/Layout/VersionStatus/index.tsx b/src/components/Layout/VersionStatus/index.tsx index e5e07869..0a2ac43c 100644 --- a/src/components/Layout/VersionStatus/index.tsx +++ b/src/components/Layout/VersionStatus/index.tsx @@ -1,3 +1,9 @@ +import { + ArrowCircleUpIcon, + BeakerIcon, + CodeIcon, + ServerIcon, +} from '@heroicons/react/outline'; import Link from 'next/link'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -51,50 +57,11 @@ const VersionStatus: React.FC = ({ onClick }) => { }`} > {data.commitTag === 'local' ? ( - - - + ) : data.version.startsWith('develop-') ? ( - - - + ) : ( - - - + )}
{versionStream} @@ -114,22 +81,7 @@ const VersionStatus: React.FC = ({ onClick }) => { )}
- {data.updateAvailable && ( - - - - )} + {data.updateAvailable && } ); diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 18519734..7ea9ac64 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -1,3 +1,5 @@ +import { MenuAlt2Icon } from '@heroicons/react/outline'; +import { InformationCircleIcon } from '@heroicons/react/solid'; import { useRouter } from 'next/router'; import React, { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -57,19 +59,7 @@ const Layout: React.FC = ({ children }) => { aria-label="Open sidebar" onClick={() => setSidebarOpen(true)} > - - - +
@@ -87,18 +77,7 @@ const Layout: React.FC = ({ children }) => {
- - - +

diff --git a/src/components/Login/LocalLogin.tsx b/src/components/Login/LocalLogin.tsx index 4216bf4a..6444635f 100644 --- a/src/components/Login/LocalLogin.tsx +++ b/src/components/Login/LocalLogin.tsx @@ -1,3 +1,4 @@ +import { LoginIcon, SupportIcon } from '@heroicons/react/outline'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import Link from 'next/link'; @@ -12,7 +13,7 @@ const messages = defineMessages({ validationemailrequired: 'You must provide a valid email address', validationpasswordrequired: 'You must provide a password', loginerror: 'Something went wrong while trying to sign in.', - signingin: 'Signing in…', + signingin: 'Signing In…', signin: 'Sign In', forgotpassword: 'Forgot Password?', }); @@ -103,6 +104,7 @@ const LocalLogin: React.FC = ({ revalidate }) => { @@ -113,6 +115,7 @@ const LocalLogin: React.FC = ({ revalidate }) => { type="submit" disabled={isSubmitting || !isValid} > + {isSubmitting ? intl.formatMessage(messages.signingin) : intl.formatMessage(messages.signin)} diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx index 0474b9da..d877e160 100644 --- a/src/components/Login/index.tsx +++ b/src/components/Login/index.tsx @@ -1,3 +1,4 @@ +import { XCircleIcon } from '@heroicons/react/solid'; import axios from 'axios'; import { useRouter } from 'next/dist/client/router'; import React, { useEffect, useState } from 'react'; @@ -100,19 +101,7 @@ const Login: React.FC = () => {

- +

diff --git a/src/components/MediaSlider/ShowMoreCard/index.tsx b/src/components/MediaSlider/ShowMoreCard/index.tsx index e229ad7e..c4290966 100644 --- a/src/components/MediaSlider/ShowMoreCard/index.tsx +++ b/src/components/MediaSlider/ShowMoreCard/index.tsx @@ -1,3 +1,4 @@ +import { ArrowCircleRightIcon } from '@heroicons/react/solid'; import Link from 'next/link'; import React, { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -79,18 +80,7 @@ const ShowMoreCard: React.FC = ({ url, posters }) => { )}

- - - +
{intl.formatMessage(messages.seemore)}
diff --git a/src/components/MediaSlider/index.tsx b/src/components/MediaSlider/index.tsx index c46e16bc..64aa7915 100644 --- a/src/components/MediaSlider/index.tsx +++ b/src/components/MediaSlider/index.tsx @@ -1,3 +1,4 @@ +import { ArrowCircleRightIcon } from '@heroicons/react/outline'; import Link from 'next/link'; import React, { useContext, useEffect } from 'react'; import { useSWRInfinite } from 'swr'; @@ -140,20 +141,7 @@ const MediaSlider: React.FC = ({ {title} - - - + ) : ( diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index dadd9c1a..7db6b946 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -1,3 +1,14 @@ +import { + ArrowCircleRightIcon, + CogIcon, + FilmIcon, + PlayIcon, +} from '@heroicons/react/outline'; +import { + CheckCircleIcon, + DocumentRemoveIcon, + ExternalLinkIcon, +} from '@heroicons/react/solid'; import axios from 'axios'; import Link from 'next/link'; import { useRouter } from 'next/router'; @@ -50,7 +61,7 @@ const messages = defineMessages({ manageModalTitle: 'Manage Movie', manageModalRequests: 'Requests', manageModalNoRequests: 'No requests.', - manageModalClearMedia: 'Clear All Media Data', + manageModalClearMedia: 'Clear Media Data', manageModalClearMediaWarning: '* This will irreversibly remove all data for this movie, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.', studio: '{studioCount, plural, one {Studio} other {Studios}}', @@ -105,6 +116,7 @@ const MovieDetails: React.FC = ({ movie }) => { mediaLinks.push({ text: intl.formatMessage(messages.playonplex), url: data.mediaInfo?.plexUrl, + svg: , }); } @@ -117,6 +129,7 @@ const MovieDetails: React.FC = ({ movie }) => { mediaLinks.push({ text: intl.formatMessage(messages.play4konplex), url: data.mediaInfo?.plexUrl4k, + svg: , }); } @@ -129,6 +142,7 @@ const MovieDetails: React.FC = ({ movie }) => { mediaLinks.push({ text: intl.formatMessage(messages.watchtrailer), url: trailerUrl, + svg: , }); } @@ -266,18 +280,7 @@ const MovieDetails: React.FC = ({ movie }) => { className="w-full sm:mb-0" buttonType="success" > - - - + {intl.formatMessage(messages.markavailable)}
@@ -291,18 +294,7 @@ const MovieDetails: React.FC = ({ movie }) => { className="w-full sm:mb-0" buttonType="success" > - - - + {intl.formatMessage(messages.mark4kavailable)} @@ -341,15 +333,7 @@ const MovieDetails: React.FC = ({ movie }) => { className="block mb-2 last:mb-0" > @@ -361,15 +345,7 @@ const MovieDetails: React.FC = ({ movie }) => { rel="noreferrer" > @@ -383,6 +359,7 @@ const MovieDetails: React.FC = ({ movie }) => { confirmText={intl.formatMessage(globalMessages.areyousure)} className="w-full" > + {intl.formatMessage(messages.manageModalClearMedia)}
@@ -463,27 +440,7 @@ const MovieDetails: React.FC = ({ movie }) => { className="ml-2 first:ml-0" onClick={() => setShowManager(true)} > - - - - + )}
@@ -513,20 +470,7 @@ const MovieDetails: React.FC = ({ movie }) => { {intl.formatMessage(messages.viewfullcrew)} - - - +
@@ -709,20 +653,7 @@ const MovieDetails: React.FC = ({ movie }) => { {intl.formatMessage(messages.cast)} - - - +
diff --git a/src/components/PersonCard/index.tsx b/src/components/PersonCard/index.tsx index 67cdf43f..85e41ff3 100644 --- a/src/components/PersonCard/index.tsx +++ b/src/components/PersonCard/index.tsx @@ -1,3 +1,4 @@ +import { UserCircleIcon } from '@heroicons/react/solid'; import Link from 'next/link'; import React, { useState } from 'react'; import CachedImage from '../Common/CachedImage'; @@ -57,18 +58,7 @@ const PersonCard: React.FC = ({ />
) : ( - - - + )}
{name}
diff --git a/src/components/PlexLoginButton/index.tsx b/src/components/PlexLoginButton/index.tsx index f6bbb5b7..c85fa78c 100644 --- a/src/components/PlexLoginButton/index.tsx +++ b/src/components/PlexLoginButton/index.tsx @@ -1,3 +1,4 @@ +import { LoginIcon } from '@heroicons/react/outline'; import React, { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import globalMessages from '../../i18n/globalMessages'; @@ -5,7 +6,7 @@ import PlexOAuth from '../../utils/plex'; const messages = defineMessages({ signinwithplex: 'Sign In', - signingin: 'Signing in…', + signingin: 'Signing In…', }); const plexOAuth = new PlexOAuth(); @@ -48,6 +49,7 @@ const PlexLoginButton: React.FC = ({ disabled={loading || isProcessing} className="plex-button" > + {loading ? intl.formatMessage(globalMessages.loading) : isProcessing diff --git a/src/components/RegionSelector/index.tsx b/src/components/RegionSelector/index.tsx index 2cbdc435..dbbae3f9 100644 --- a/src/components/RegionSelector/index.tsx +++ b/src/components/RegionSelector/index.tsx @@ -1,11 +1,12 @@ -import React, { useEffect, useMemo, useState } from 'react'; import { Listbox, Transition } from '@headlessui/react'; -import useSWR from 'swr'; -import type { Region } from '../../../server/lib/settings'; -import { defineMessages, useIntl } from 'react-intl'; -import useSettings from '../../hooks/useSettings'; +import { CheckIcon, ChevronDownIcon } from '@heroicons/react/solid'; import { hasFlag } from 'country-flag-icons'; import 'country-flag-icons/3x2/flags.css'; +import React, { useEffect, useMemo, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import useSWR from 'swr'; +import type { Region } from '../../../server/lib/settings'; +import useSettings from '../../hooks/useSettings'; const messages = defineMessages({ regionDefault: 'All Regions', @@ -125,20 +126,7 @@ const RegionSelector: React.FC = ({ : intl.formatMessage(messages.regionDefault)} - - - + @@ -196,18 +184,7 @@ const RegionSelector: React.FC = ({ active ? 'text-white' : 'text-indigo-600' } absolute inset-y-0 left-0 flex items-center pl-1.5`} > - - - + )}
@@ -234,18 +211,7 @@ const RegionSelector: React.FC = ({ active ? 'text-white' : 'text-indigo-600' } absolute inset-y-0 left-0 flex items-center pl-1.5`} > - - - + )}
@@ -286,18 +252,7 @@ const RegionSelector: React.FC = ({ active ? 'text-white' : 'text-indigo-600' } absolute inset-y-0 left-0 flex items-center pl-1.5`} > - - - + )} diff --git a/src/components/RequestBlock/index.tsx b/src/components/RequestBlock/index.tsx index c23370e9..d003bcf7 100644 --- a/src/components/RequestBlock/index.tsx +++ b/src/components/RequestBlock/index.tsx @@ -1,3 +1,12 @@ +import { + CalendarIcon, + CheckIcon, + EyeIcon, + PencilIcon, + TrashIcon, + UserIcon, + XIcon, +} from '@heroicons/react/solid'; import axios from 'axios'; import React, { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -69,37 +78,14 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => {
- - - + {request.requestedBy.displayName}
{request.modifiedBy && (
- - - - + {request.modifiedBy?.displayName} @@ -115,18 +101,7 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => { onClick={() => updateRequest('approve')} disabled={isUpdating} > - - - + @@ -135,18 +110,7 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => { onClick={() => updateRequest('decline')} disabled={isUpdating} > - - - + @@ -155,14 +119,7 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => { onClick={() => setShowEditModal(true)} disabled={isUpdating} > - - - + @@ -173,18 +130,7 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => { onClick={() => deleteRequest()} disabled={isUpdating} > - - - + )}
@@ -215,18 +161,7 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => {
- - - + {intl.formatDate(request.createdAt, { year: 'numeric', diff --git a/src/components/RequestButton/index.tsx b/src/components/RequestButton/index.tsx index 2c26c694..a528bba4 100644 --- a/src/components/RequestButton/index.tsx +++ b/src/components/RequestButton/index.tsx @@ -1,5 +1,11 @@ +import { DownloadIcon } from '@heroicons/react/outline'; +import { + CheckIcon, + InformationCircleIcon, + XIcon, +} from '@heroicons/react/solid'; import axios from 'axios'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { MediaRequestStatus, @@ -17,19 +23,19 @@ const messages = defineMessages({ viewrequest: 'View Request', viewrequest4k: 'View 4K Request', requestmore: 'Request More', - requestmore4k: 'Request More 4K', + requestmore4k: 'Request More in 4K', approverequest: 'Approve Request', approverequest4k: 'Approve 4K Request', declinerequest: 'Decline Request', declinerequest4k: 'Decline 4K Request', approverequests: - 'Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}', + 'Approve {requestCount, plural, one {Request} other {{requestCount} Requests}}', declinerequests: - 'Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}', + 'Decline {requestCount, plural, one {Request} other {{requestCount} Requests}}', approve4krequests: - 'Approve {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}', + 'Approve {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}', decline4krequests: - 'Decline {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}', + 'Decline {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}', }); interface ButtonOption { @@ -58,26 +64,34 @@ const RequestButton: React.FC = ({ }) => { const intl = useIntl(); const settings = useSettings(); - const { hasPermission } = useUser(); + const { user, hasPermission } = useUser(); const [showRequestModal, setShowRequestModal] = useState(false); const [showRequest4kModal, setShowRequest4kModal] = useState(false); + const [editRequest, setEditRequest] = useState(false); - const activeRequest = media?.requests.find( - (request) => request.status === MediaRequestStatus.PENDING && !request.is4k - ); - const active4kRequest = media?.requests.find( - (request) => request.status === MediaRequestStatus.PENDING && request.is4k - ); - - // All pending + // All pending requests const activeRequests = media?.requests.filter( (request) => request.status === MediaRequestStatus.PENDING && !request.is4k ); - const active4kRequests = media?.requests.filter( (request) => request.status === MediaRequestStatus.PENDING && request.is4k ); + const activeRequest = useMemo(() => { + return activeRequests && activeRequests.length > 0 + ? activeRequests.find((request) => request.requestedBy.id === user?.id) ?? + activeRequests[0] + : undefined; + }, [activeRequests, user]); + + const active4kRequest = useMemo(() => { + return active4kRequests && active4kRequests.length > 0 + ? active4kRequests.find( + (request) => request.requestedBy.id === user?.id + ) ?? active4kRequests[0] + : undefined; + }, [active4kRequests, user]); + const modifyRequest = async ( request: MediaRequest, type: 'approve' | 'decline' @@ -115,57 +129,10 @@ const RequestButton: React.FC = ({ id: 'request', text: intl.formatMessage(globalMessages.request), action: () => { + setEditRequest(false); setShowRequestModal(true); }, - svg: ( - - - - ), - }); - } - - if ( - hasPermission(Permission.REQUEST) && - mediaType === 'tv' && - media && - media.status !== MediaStatus.AVAILABLE && - media.status !== MediaStatus.UNKNOWN && - !isShowComplete - ) { - buttons.push({ - id: 'request-more', - text: intl.formatMessage(messages.requestmore), - action: () => { - setShowRequestModal(true); - }, - svg: ( - - - - ), + svg: , }); } @@ -181,112 +148,44 @@ const RequestButton: React.FC = ({ id: 'request4k', text: intl.formatMessage(globalMessages.request4k), action: () => { + setEditRequest(false); setShowRequest4kModal(true); }, - svg: ( - - - - ), - }); - } - - if ( - mediaType === 'tv' && - (hasPermission(Permission.REQUEST_4K) || - (mediaType === 'tv' && hasPermission(Permission.REQUEST_4K_TV))) && - media && - media.status4k !== MediaStatus.AVAILABLE && - media.status4k !== MediaStatus.UNKNOWN && - !is4kShowComplete && - settings.currentSettings.series4kEnabled - ) { - buttons.push({ - id: 'request-more-4k', - text: intl.formatMessage(messages.requestmore4k), - action: () => { - setShowRequest4kModal(true); - }, - svg: ( - - - - ), + svg: , }); } if ( activeRequest && - mediaType === 'movie' && - hasPermission(Permission.REQUEST) + (activeRequest.requestedBy.id === user?.id || + (activeRequests?.length === 1 && + hasPermission(Permission.MANAGE_REQUESTS))) ) { buttons.push({ id: 'active-request', text: intl.formatMessage(messages.viewrequest), - action: () => setShowRequestModal(true), - svg: ( - - - - ), + action: () => { + setEditRequest(true); + setShowRequestModal(true); + }, + svg: , }); } if ( active4kRequest && - mediaType === 'movie' && - (hasPermission(Permission.REQUEST_4K) || - hasPermission(Permission.REQUEST_4K_MOVIE)) + (active4kRequest.requestedBy.id === user?.id || + (active4kRequests?.length === 1 && + hasPermission(Permission.MANAGE_REQUESTS))) ) { buttons.push({ id: 'active-4k-request', text: intl.formatMessage(messages.viewrequest4k), - action: () => setShowRequest4kModal(true), - svg: ( - - - - ), + action: () => { + setEditRequest(true); + setShowRequest4kModal(true); + }, + svg: , }); } @@ -302,20 +201,7 @@ const RequestButton: React.FC = ({ action: () => { modifyRequest(activeRequest, 'approve'); }, - svg: ( - - - - ), + svg: , }, { id: 'decline-request', @@ -323,20 +209,7 @@ const RequestButton: React.FC = ({ action: () => { modifyRequest(activeRequest, 'decline'); }, - svg: ( - - - - ), + svg: , } ); } @@ -356,20 +229,7 @@ const RequestButton: React.FC = ({ action: () => { modifyRequests(activeRequests, 'approve'); }, - svg: ( - - - - ), + svg: , }, { id: 'decline-request-batch', @@ -379,20 +239,7 @@ const RequestButton: React.FC = ({ action: () => { modifyRequests(activeRequests, 'decline'); }, - svg: ( - - - - ), + svg: , } ); } @@ -409,20 +256,7 @@ const RequestButton: React.FC = ({ action: () => { modifyRequest(active4kRequest, 'approve'); }, - svg: ( - - - - ), + svg: , }, { id: 'decline-4k-request', @@ -430,20 +264,7 @@ const RequestButton: React.FC = ({ action: () => { modifyRequest(active4kRequest, 'decline'); }, - svg: ( - - - - ), + svg: , } ); } @@ -456,54 +277,71 @@ const RequestButton: React.FC = ({ ) { buttons.push( { - id: 'approve-request-batch', + id: 'approve-4k-request-batch', text: intl.formatMessage(messages.approve4krequests, { requestCount: active4kRequests.length, }), action: () => { modifyRequests(active4kRequests, 'approve'); }, - svg: ( - - - - ), + svg: , }, { - id: 'decline-request-batch', + id: 'decline-4k-request-batch', text: intl.formatMessage(messages.decline4krequests, { requestCount: active4kRequests.length, }), action: () => { modifyRequests(active4kRequests, 'decline'); }, - svg: ( - - - - ), + svg: , } ); } + if ( + mediaType === 'tv' && + (!activeRequest || activeRequest.requestedBy.id !== user?.id) && + hasPermission(Permission.REQUEST) && + media && + media.status !== MediaStatus.AVAILABLE && + media.status !== MediaStatus.UNKNOWN && + !isShowComplete + ) { + buttons.push({ + id: 'request-more', + text: intl.formatMessage(messages.requestmore), + action: () => { + setEditRequest(false); + setShowRequestModal(true); + }, + svg: , + }); + } + + if ( + mediaType === 'tv' && + (!active4kRequest || active4kRequest.requestedBy.id !== user?.id) && + hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_TV], { + type: 'or', + }) && + media && + media.status4k !== MediaStatus.AVAILABLE && + media.status4k !== MediaStatus.UNKNOWN && + !is4kShowComplete && + settings.currentSettings.series4kEnabled + ) { + buttons.push({ + id: 'request-more-4k', + text: intl.formatMessage(messages.requestmore4k), + action: () => { + setEditRequest(false); + setShowRequest4kModal(true); + }, + svg: , + }); + } + const [buttonOne, ...others] = buttons; if (!buttonOne) { @@ -516,6 +354,7 @@ const RequestButton: React.FC = ({ tmdbId={tmdbId} show={showRequestModal} type={mediaType} + editRequest={editRequest ? activeRequest : undefined} onComplete={() => { onUpdate(); setShowRequestModal(false); @@ -526,6 +365,7 @@ const RequestButton: React.FC = ({ tmdbId={tmdbId} show={showRequest4kModal} type={mediaType} + editRequest={editRequest ? active4kRequest : undefined} is4k onComplete={() => { onUpdate(); diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index e6baf6b1..7e71813e 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -1,3 +1,4 @@ +import { CheckIcon, TrashIcon, XIcon } from '@heroicons/react/solid'; import axios from 'axios'; import Link from 'next/link'; import React, { useContext, useEffect } from 'react'; @@ -68,20 +69,7 @@ const RequestCardError: React.FC = ({ mediaId }) => { buttonSize="sm" onClick={() => deleteRequest()} > - - - + {intl.formatMessage(messages.deleterequest)}
@@ -261,18 +249,7 @@ const RequestCard: React.FC = ({ request, onTitleData }) => { buttonSize="sm" onClick={() => modifyRequest('approve')} > - - - + {intl.formatMessage(globalMessages.approve)} @@ -284,18 +261,7 @@ const RequestCard: React.FC = ({ request, onTitleData }) => { buttonSize="sm" onClick={() => modifyRequest('decline')} > - - - + {intl.formatMessage(globalMessages.decline)} diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index f9f512c9..01fb1ddc 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -1,3 +1,10 @@ +import { + CheckIcon, + PencilIcon, + RefreshIcon, + TrashIcon, + XIcon, +} from '@heroicons/react/solid'; import axios from 'axios'; import Link from 'next/link'; import React, { useContext, useState } from 'react'; @@ -29,6 +36,7 @@ const messages = defineMessages({ modified: 'Modified', modifieduserdate: '{date} by {user}', mediaerror: 'The associated title for this request is no longer available.', + editrequest: 'Edit Request', deleterequest: 'Delete Request', cancelRequest: 'Cancel Request', }); @@ -66,20 +74,7 @@ const RequestItemError: React.FC = ({ buttonSize="sm" onClick={() => deleteRequest()} > - - - + {intl.formatMessage(messages.deleterequest)} @@ -369,31 +364,6 @@ const RequestItem: React.FC = ({
- {requestData.status === MediaRequestStatus.PENDING && - !hasPermission(Permission.MANAGE_REQUESTS) && - requestData.requestedBy.id === user?.id && ( - deleteRequest()} - confirmText={intl.formatMessage(globalMessages.areyousure)} - className="w-full" - > - - - - - {intl.formatMessage(messages.cancelRequest)} - - - )} {requestData.media[requestData.is4k ? 'status4k' : 'status'] === MediaStatus.UNKNOWN && requestData.status !== MediaRequestStatus.DECLINED && @@ -404,19 +374,14 @@ const RequestItem: React.FC = ({ disabled={isRetrying} onClick={() => retryRequest()} > - - - - + - {intl.formatMessage(globalMessages.retry)} + {intl.formatMessage( + isRetrying ? globalMessages.retrying : globalMessages.retry + )} )} @@ -427,94 +392,72 @@ const RequestItem: React.FC = ({ confirmText={intl.formatMessage(globalMessages.areyousure)} className="w-full" > - - - + - {intl.formatMessage(globalMessages.delete)} + {intl.formatMessage(messages.deleterequest)} )} {requestData.status === MediaRequestStatus.PENDING && hasPermission(Permission.MANAGE_REQUESTS) && ( - <> -
- - - - - - -
+
- + + + +
+ )} + {requestData.status === MediaRequestStatus.PENDING && + (hasPermission(Permission.MANAGE_REQUESTS) || + (requestData.requestedBy.id === user?.id && + (requestData.type === 'tv' || + hasPermission(Permission.REQUEST_ADVANCED)))) && ( + + + + )} + {requestData.status === MediaRequestStatus.PENDING && + !hasPermission(Permission.MANAGE_REQUESTS) && + requestData.requestedBy.id === user?.id && ( + deleteRequest()} + confirmText={intl.formatMessage(globalMessages.areyousure)} + className="w-full" + > + + + {intl.formatMessage(messages.cancelRequest)} + + )}
diff --git a/src/components/RequestList/index.tsx b/src/components/RequestList/index.tsx index d0177e64..fd328101 100644 --- a/src/components/RequestList/index.tsx +++ b/src/components/RequestList/index.tsx @@ -1,3 +1,4 @@ +import { FilterIcon, SortDescendingIcon } from '@heroicons/react/solid'; import Link from 'next/link'; import { useRouter } from 'next/router'; import React, { useEffect, useState } from 'react'; @@ -119,18 +120,7 @@ const RequestList: React.FC = () => {
- - - + = ({ return ( <>
- - - - + {intl.formatMessage(messages.advancedoptions)}
@@ -526,19 +523,7 @@ const AdvancedRequester: React.FC = ({ - - - + @@ -592,18 +577,7 @@ const AdvancedRequester: React.FC = ({ : 'text-indigo-600' } absolute inset-y-0 left-0 flex items-center pl-1.5`} > - - - + )}
diff --git a/src/components/RequestModal/MovieRequestModal.tsx b/src/components/RequestModal/MovieRequestModal.tsx index 536dc5a7..bd2512ae 100644 --- a/src/components/RequestModal/MovieRequestModal.tsx +++ b/src/components/RequestModal/MovieRequestModal.tsx @@ -1,17 +1,14 @@ +import { DownloadIcon } from '@heroicons/react/outline'; import axios from 'axios'; import React, { useCallback, useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; -import { - MediaRequestStatus, - MediaStatus, -} from '../../../server/constants/media'; +import { MediaStatus } from '../../../server/constants/media'; import { MediaRequest } from '../../../server/entity/MediaRequest'; import { QuotaResponse } from '../../../server/interfaces/api/userInterfaces'; import { Permission } from '../../../server/lib/permissions'; import { MovieDetails } from '../../../server/models/Movie'; -import DownloadIcon from '../../assets/download.svg'; import { useUser } from '../../hooks/useUser'; import globalMessages from '../../i18n/globalMessages'; import Alert from '../Common/Alert'; @@ -25,11 +22,11 @@ const messages = defineMessages({ requestCancel: 'Request for {title} canceled.', requesttitle: 'Request {title}', request4ktitle: 'Request {title} in 4K', + edit: 'Edit Request', cancel: 'Cancel Request', pendingrequest: 'Pending Request for {title}', - pending4krequest: 'Pending Request for {title} in 4K', - requestfrom: 'There is currently a pending request from {username}.', - request4kfrom: 'There is currently a pending 4K request from {username}.', + pending4krequest: 'Pending 4K Request for {title}', + requestfrom: "{username}'s request is pending approval.", errorediting: 'Something went wrong while editing the request.', requestedited: 'Request for {title} edited successfully!', requesterror: 'Something went wrong while submitting the request.', @@ -130,18 +127,14 @@ const MovieRequestModal: React.FC = ({ } finally { setIsUpdating(false); } - }, [data, onComplete, addToast, requestOverrides]); - - const activeRequest = data?.mediaInfo?.requests?.find( - (request) => request.is4k === !!is4k - ); + }, [data, onComplete, addToast, requestOverrides, hasPermission, intl, is4k]); const cancelRequest = async () => { setIsUpdating(true); try { const response = await axios.delete( - `/api/v1/request/${activeRequest?.id}` + `/api/v1/request/${editRequest?.id}` ); if (response.status === 204) { @@ -206,11 +199,15 @@ const MovieRequestModal: React.FC = ({ } }; - const isOwner = activeRequest - ? activeRequest.requestedBy.id === user?.id - : false; + if (editRequest) { + const isOwner = editRequest.requestedBy.id === user?.id; + const showEditButton = hasPermission( + [Permission.MANAGE_REQUESTS, Permission.REQUEST_ADVANCED], + { + type: 'or', + } + ); - if (activeRequest?.status === MediaRequestStatus.PENDING) { return ( = ({ onCancel={onCancel} title={intl.formatMessage( is4k ? messages.pending4krequest : messages.pendingrequest, - { - title: data?.title, - } + { title: data?.title } )} - onOk={() => (isOwner ? cancelRequest() : updateRequest())} + onOk={() => (showEditButton ? updateRequest() : cancelRequest())} okDisabled={isUpdating} okText={ - isOwner - ? isUpdating - ? intl.formatMessage(globalMessages.canceling) - : intl.formatMessage(messages.cancel) - : intl.formatMessage(globalMessages.edit) + showEditButton + ? intl.formatMessage(messages.edit) + : intl.formatMessage(messages.cancel) } - okButtonType={isOwner ? 'danger' : 'primary'} + okButtonType={showEditButton ? 'primary' : 'danger'} + onSecondary={ + isOwner && showEditButton ? () => cancelRequest() : undefined + } + secondaryDisabled={isUpdating} + secondaryText={ + isOwner && showEditButton + ? intl.formatMessage(messages.cancel) + : undefined + } + secondaryButtonType="danger" cancelText={intl.formatMessage(globalMessages.close)} iconSvg={} > {isOwner ? intl.formatMessage(messages.pendingapproval) - : intl.formatMessage( - is4k ? messages.request4kfrom : messages.requestfrom, - { - username: activeRequest.requestedBy.displayName, - } - )} + : intl.formatMessage(messages.requestfrom, { + username: editRequest.requestedBy.displayName, + })} {(hasPermission(Permission.REQUEST_ADVANCED) || hasPermission(Permission.MANAGE_REQUESTS)) && (
{ setRequestOverrides(overrides); }} diff --git a/src/components/RequestModal/QuotaDisplay/index.tsx b/src/components/RequestModal/QuotaDisplay/index.tsx index 5d3decdc..223540d5 100644 --- a/src/components/RequestModal/QuotaDisplay/index.tsx +++ b/src/components/RequestModal/QuotaDisplay/index.tsx @@ -1,3 +1,4 @@ +import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'; import Link from 'next/link'; import React, { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -90,31 +91,9 @@ const QuotaDisplay: React.FC = ({
{showDetails ? ( - - - + ) : ( - - - + )}
diff --git a/src/components/RequestModal/SearchByNameModal/index.tsx b/src/components/RequestModal/SearchByNameModal/index.tsx index 50df0469..111d6137 100644 --- a/src/components/RequestModal/SearchByNameModal/index.tsx +++ b/src/components/RequestModal/SearchByNameModal/index.tsx @@ -1,3 +1,4 @@ +import { DownloadIcon } from '@heroicons/react/outline'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; @@ -51,22 +52,7 @@ const SearchByNameModal: React.FC = ({ okText={intl.formatMessage(globalMessages.next)} okDisabled={!tvdbId} okButtonType="primary" - iconSvg={ - - - - } + iconSvg={} > {title} requested successfully!', requesttitle: 'Request {title}', request4ktitle: 'Request {title} in 4K', + edit: 'Edit Request', + cancel: 'Cancel Request', + pendingrequest: 'Pending Request for {title}', + pending4krequest: 'Pending 4K Request for {title}', + requestfrom: "{username}'s request is pending approval.", requestseasons: 'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}', requestall: 'Request All Seasons', @@ -42,6 +48,7 @@ const messages = defineMessages({ requestcancelled: 'Request for {title} canceled.', autoapproval: 'Automatic Approval', requesterror: 'Something went wrong while submitting the request.', + pendingapproval: 'Your request is pending approval.', }); interface RequestModalProps extends React.HTMLAttributes { @@ -341,6 +348,8 @@ const TvRequestModal: React.FC = ({ return seasonRequest; }; + const isOwner = editRequest && editRequest.requestedBy.id === user?.id; + return !data?.externalIds.tvdbId && searchModal.show ? ( = ({ onCancel={tvdbId ? () => setSearchModal({ show: true }) : onCancel} onOk={() => (editRequest ? updateRequest() : sendRequest())} title={intl.formatMessage( - is4k ? messages.request4ktitle : messages.requesttitle, + editRequest + ? is4k + ? messages.pending4krequest + : messages.pendingrequest + : is4k + ? messages.request4ktitle + : messages.requesttitle, { title: data?.name } )} okText={ - editRequest && selectedSeasons.length === 0 - ? 'Cancel Request' + editRequest + ? selectedSeasons.length === 0 + ? intl.formatMessage(messages.cancel) + : intl.formatMessage(messages.edit) : getAllRequestedSeasons().length >= getAllSeasons().length ? intl.formatMessage(messages.alreadyrequested) : !settings.currentSettings.partialRequestsEnabled @@ -396,27 +413,21 @@ const TvRequestModal: React.FC = ({ : `primary` } cancelText={ - tvdbId + editRequest + ? intl.formatMessage(globalMessages.close) + : tvdbId ? intl.formatMessage(globalMessages.back) : intl.formatMessage(globalMessages.cancel) } - iconSvg={ - - - - } + iconSvg={} > + {editRequest + ? isOwner + ? intl.formatMessage(messages.pendingapproval) + : intl.formatMessage(messages.requestfrom, { + username: editRequest?.requestedBy.displayName, + }) + : null} {hasPermission( [ Permission.MANAGE_REQUESTS, diff --git a/src/components/ResetPassword/RequestResetLink.tsx b/src/components/ResetPassword/RequestResetLink.tsx index f9359772..74c342fa 100644 --- a/src/components/ResetPassword/RequestResetLink.tsx +++ b/src/components/ResetPassword/RequestResetLink.tsx @@ -1,3 +1,4 @@ +import { AtSymbolIcon } from '@heroicons/react/outline'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import Link from 'next/link'; @@ -123,6 +124,7 @@ const ResetPassword: React.FC = () => { type="submit" disabled={isSubmitting || !isValid} > + {intl.formatMessage(messages.emailresetlink)} diff --git a/src/components/Settings/CopyButton.tsx b/src/components/Settings/CopyButton.tsx index abc5d09d..4ae21190 100644 --- a/src/components/Settings/CopyButton.tsx +++ b/src/components/Settings/CopyButton.tsx @@ -1,7 +1,8 @@ +import { ClipboardCopyIcon } from '@heroicons/react/solid'; import React, { useEffect } from 'react'; -import useClipboard from 'react-use-clipboard'; -import { useToasts } from 'react-toast-notifications'; import { defineMessages, useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; +import useClipboard from 'react-use-clipboard'; const messages = defineMessages({ copied: 'Copied API key to clipboard.', @@ -29,17 +30,9 @@ const CopyButton: React.FC<{ textToCopy: string }> = ({ textToCopy }) => { e.preventDefault(); setCopied(); }} - className="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-500 text-sm leading-5 font-medium text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150" + className="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium leading-5 text-white transition duration-150 ease-in-out bg-indigo-600 border border-gray-500 hover:bg-indigo-500 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700" > - - - - + ); }; diff --git a/src/components/Settings/LibraryItem.tsx b/src/components/Settings/LibraryItem.tsx index d7586454..a1accde4 100644 --- a/src/components/Settings/LibraryItem.tsx +++ b/src/components/Settings/LibraryItem.tsx @@ -1,3 +1,4 @@ +import { CheckIcon, XIcon } from '@heroicons/react/solid'; import React from 'react'; interface LibraryItemProps { @@ -12,8 +13,8 @@ const LibraryItem: React.FC = ({ onToggle, }) => { return ( -
  • -
    +
  • +
    {name}
    @@ -45,19 +46,7 @@ const LibraryItem: React.FC = ({ : 'opacity-100 ease-in duration-200' } absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`} > - - - + = ({ : 'opacity-0 ease-out duration-100' } absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`} > - - - + diff --git a/src/components/Settings/Notifications/NotificationsDiscord.tsx b/src/components/Settings/Notifications/NotificationsDiscord.tsx index ecaff4b0..b70baf28 100644 --- a/src/components/Settings/Notifications/NotificationsDiscord.tsx +++ b/src/components/Settings/Notifications/NotificationsDiscord.tsx @@ -18,7 +18,7 @@ const messages = defineMessages({ webhookUrlPlaceholder: 'Server Settings → Integrations → Webhooks', discordsettingssaved: 'Discord notification settings saved successfully!', discordsettingsfailed: 'Discord notification settings failed to save.', - testsent: 'Discord test notification sent!', + discordtestsent: 'Discord test notification sent!', validationUrl: 'You must provide a valid URL', }); @@ -96,7 +96,7 @@ const NotificationsDiscord: React.FC = () => { }, }); - addToast(intl.formatMessage(messages.testsent), { + addToast(intl.formatMessage(messages.discordtestsent), { appearance: 'info', autoDismiss: true, }); diff --git a/src/components/Settings/Notifications/NotificationsEmail.tsx b/src/components/Settings/Notifications/NotificationsEmail.tsx index 1daca614..ac45e3e6 100644 --- a/src/components/Settings/Notifications/NotificationsEmail.tsx +++ b/src/components/Settings/Notifications/NotificationsEmail.tsx @@ -24,7 +24,7 @@ const messages = defineMessages({ authPass: 'SMTP Password', emailsettingssaved: 'Email notification settings saved successfully!', emailsettingsfailed: 'Email notification settings failed to save.', - testsent: 'Email test notification sent!', + emailtestsent: 'Email test notification sent!', allowselfsigned: 'Allow Self-Signed Certificates', ssldisabletip: 'SSL should be disabled on standard TLS connections (port 587)', @@ -188,7 +188,7 @@ const NotificationsEmail: React.FC = () => { }, }); - addToast(intl.formatMessage(messages.testsent), { + addToast(intl.formatMessage(messages.emailtestsent), { appearance: 'info', autoDismiss: true, }); diff --git a/src/components/Settings/Notifications/NotificationsTelegram.tsx b/src/components/Settings/Notifications/NotificationsTelegram.tsx index 72b8d4dd..6f8c230b 100644 --- a/src/components/Settings/Notifications/NotificationsTelegram.tsx +++ b/src/components/Settings/Notifications/NotificationsTelegram.tsx @@ -22,7 +22,7 @@ const messages = defineMessages({ validationChatIdRequired: 'You must provide a valid chat ID', telegramsettingssaved: 'Telegram notification settings saved successfully!', telegramsettingsfailed: 'Telegram notification settings failed to save.', - testsent: 'Telegram test notification sent!', + telegramtestsent: 'Telegram test notification sent!', settinguptelegramDescription: 'To configure Telegram notifications, you will need to create a bot and get the bot API key. Additionally, you will need the chat ID for the chat to which you would like to send notifications. You can find this by adding @get_id_bot to the chat and issuing the /my_id command.', sendSilently: 'Send Silently', @@ -113,7 +113,7 @@ const NotificationsTelegram: React.FC = () => { }, }); - addToast(intl.formatMessage(messages.testsent), { + addToast(intl.formatMessage(messages.telegramtestsent), { appearance: 'info', autoDismiss: true, }); diff --git a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx index 5ad3bacf..8b6dffad 100644 --- a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx +++ b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx @@ -1,3 +1,4 @@ +import { QuestionMarkCircleIcon, RefreshIcon } from '@heroicons/react/solid'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import dynamic from 'next/dynamic'; @@ -232,18 +233,7 @@ const NotificationsWebhook: React.FC = () => { }} className="mr-2" > - - - + {intl.formatMessage(messages.resetPayload)} { rel="noreferrer" className="inline-flex items-center justify-center font-medium leading-5 text-white transition duration-150 ease-in-out bg-indigo-600 border border-transparent rounded-md focus:outline-none hover:bg-indigo-500 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700 disabled:opacity-50 px-2.5 py-1.5 text-xs" > - - - + {intl.formatMessage(messages.templatevariablehelp)}
    diff --git a/src/components/Settings/RadarrModal/index.tsx b/src/components/Settings/RadarrModal/index.tsx index bf3b0f20..5ff9b711 100644 --- a/src/components/Settings/RadarrModal/index.tsx +++ b/src/components/Settings/RadarrModal/index.tsx @@ -1,3 +1,4 @@ +import { PencilIcon, PlusIcon } from '@heroicons/react/solid'; import axios from 'axios'; import { Field, Formik } from 'formik'; import dynamic from 'next/dynamic'; @@ -62,7 +63,7 @@ const messages = defineMessages({ loadingTags: 'Loading tags…', testFirstTags: 'Test connection to load tags', tags: 'Tags', - preventSearch: 'Disable Auto-Search', + enableSearch: 'Enable Automatic Search', validationApplicationUrl: 'You must provide a valid URL', validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', validationBaseUrlLeadingSlash: 'Base URL must have a leading slash', @@ -256,8 +257,8 @@ const RadarrModal: React.FC = ({ isDefault: radarr?.isDefault ?? false, is4k: radarr?.is4k ?? false, externalUrl: radarr?.externalUrl, - syncEnabled: radarr?.syncEnabled, - preventSearch: radarr?.preventSearch, + syncEnabled: radarr?.syncEnabled ?? false, + enableSearch: !radarr?.preventSearch, }} validationSchema={RadarrSettingsSchema} onSubmit={async (values) => { @@ -282,7 +283,7 @@ const RadarrModal: React.FC = ({ isDefault: values.isDefault, externalUrl: values.externalUrl, syncEnabled: values.syncEnabled, - preventSearch: values.preventSearch, + preventSearch: !values.enableSearch, }; if (!radarr) { await axios.post('/api/v1/settings/radarr', submission); @@ -356,6 +357,13 @@ const RadarrModal: React.FC = ({ values.is4k ? messages.edit4kradarr : messages.editradarr ) } + iconSvg={ + !radarr ? ( + + ) : ( + + ) + } >
    @@ -701,14 +709,14 @@ const RadarrModal: React.FC = ({
    -
    diff --git a/src/components/Settings/SettingsAbout/Releases/index.tsx b/src/components/Settings/SettingsAbout/Releases/index.tsx index 1230185e..49a0c88b 100644 --- a/src/components/Settings/SettingsAbout/Releases/index.tsx +++ b/src/components/Settings/SettingsAbout/Releases/index.tsx @@ -1,3 +1,4 @@ +import { DocumentTextIcon } from '@heroicons/react/outline'; import React, { useState } from 'react'; import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import ReactMarkdown from 'react-markdown'; @@ -70,22 +71,7 @@ const Release: React.FC = ({ > setModalOpen(false)} - iconSvg={ - - - - } + iconSvg={} title={intl.formatMessage(messages.versionChangelog)} cancelText={intl.formatMessage(globalMessages.close)} okText={intl.formatMessage(messages.viewongithub)} @@ -126,6 +112,7 @@ const Release: React.FC = ({
  • diff --git a/src/components/Settings/SettingsJobsCache/index.tsx b/src/components/Settings/SettingsJobsCache/index.tsx index 46d0cd9d..992e3ac4 100644 --- a/src/components/Settings/SettingsJobsCache/index.tsx +++ b/src/components/Settings/SettingsJobsCache/index.tsx @@ -1,3 +1,4 @@ +import { PlayIcon, StopIcon, TrashIcon } from '@heroicons/react/outline'; import axios from 'axios'; import React from 'react'; import { @@ -146,12 +147,12 @@ const SettingsJobs: React.FC = () => {
    - {job.running && } {intl.formatMessage( messages[job.id] ?? messages.unknownJob )} + {job.running && }
    @@ -180,10 +181,12 @@ const SettingsJobs: React.FC = () => { {job.running ? ( ) : ( )} @@ -223,6 +226,7 @@ const SettingsJobs: React.FC = () => { {formatBytes(cache.stats.vsize)} diff --git a/src/components/Settings/SettingsLogs/index.tsx b/src/components/Settings/SettingsLogs/index.tsx index fe1845c2..a4aaf755 100644 --- a/src/components/Settings/SettingsLogs/index.tsx +++ b/src/components/Settings/SettingsLogs/index.tsx @@ -1,3 +1,10 @@ +import { + ClipboardCopyIcon, + DocumentSearchIcon, + FilterIcon, + PauseIcon, + PlayIcon, +} from '@heroicons/react/solid'; import copy from 'copy-to-clipboard'; import { useRouter } from 'next/router'; import React, { useEffect, useState } from 'react'; @@ -135,6 +142,7 @@ const SettingsLogs: React.FC = () => { > } onCancel={() => setActiveLog(null)} cancelText={intl.formatMessage(globalMessages.close)} onOk={() => (activeLog ? copyLogString(activeLog) : undefined)} @@ -237,31 +245,9 @@ const SettingsLogs: React.FC = () => { > {refreshInterval ? ( - - - + ) : ( - - - + )} @@ -273,18 +259,7 @@ const SettingsLogs: React.FC = () => {
    - - - + { onClick={() => setShowBulkEditModal(true)} disabled={selectedUsers.length === 0} > + {intl.formatMessage(messages.bulkedit)} )} diff --git a/src/components/UserProfile/ProfileHeader/index.tsx b/src/components/UserProfile/ProfileHeader/index.tsx index 774935a7..a7f26f4c 100644 --- a/src/components/UserProfile/ProfileHeader/index.tsx +++ b/src/components/UserProfile/ProfileHeader/index.tsx @@ -1,3 +1,4 @@ +import { CogIcon, UserIcon } from '@heroicons/react/solid'; import Link from 'next/link'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -92,18 +93,7 @@ const ProfileHeader: React.FC = ({ passHref > @@ -116,18 +106,7 @@ const ProfileHeader: React.FC = ({ passHref > diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx index b52db481..e0d68421 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx @@ -1,3 +1,4 @@ +import { AtSymbolIcon } from '@heroicons/react/outline'; import { useRouter } from 'next/router'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -33,20 +34,7 @@ const UserNotificationSettings: React.FC = ({ children }) => { text: intl.formatMessage(messages.email), content: ( - - - + {intl.formatMessage(messages.email)} ), diff --git a/src/components/UserProfile/index.tsx b/src/components/UserProfile/index.tsx index f9a6d311..e4dd601d 100644 --- a/src/components/UserProfile/index.tsx +++ b/src/components/UserProfile/index.tsx @@ -1,3 +1,4 @@ +import { ArrowCircleRightIcon } from '@heroicons/react/outline'; import Link from 'next/link'; import { useRouter } from 'next/router'; import React, { useCallback, useEffect, useState } from 'react'; @@ -236,20 +237,7 @@ const UserProfile: React.FC = () => { {intl.formatMessage(messages.recentrequests)} - - - +
    diff --git a/src/i18n/globalMessages.ts b/src/i18n/globalMessages.ts index 884acc4a..4c0ef790 100644 --- a/src/i18n/globalMessages.ts +++ b/src/i18n/globalMessages.ts @@ -9,7 +9,7 @@ const globalMessages = defineMessages({ requested: 'Requested', requesting: 'Requesting…', request: 'Request', - request4k: 'Request 4K', + request4k: 'Request in 4K', failed: 'Failed', pending: 'Pending', declined: 'Declined', @@ -24,6 +24,7 @@ const globalMessages = defineMessages({ decline: 'Decline', delete: 'Delete', retry: 'Retry', + retrying: 'Retrying…', view: 'View', deleting: 'Deleting…', test: 'Test', diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 60c7ce3e..c095ced7 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -52,7 +52,7 @@ "components.Login.loginerror": "Something went wrong while trying to sign in.", "components.Login.password": "Password", "components.Login.signin": "Sign In", - "components.Login.signingin": "Signing in…", + "components.Login.signingin": "Signing In…", "components.Login.signinheader": "Sign in to continue", "components.Login.signinwithoverseerr": "Use your {applicationTitle} account", "components.Login.signinwithplex": "Use your Plex account", @@ -64,7 +64,7 @@ "components.MovieDetails.budget": "Budget", "components.MovieDetails.cast": "Cast", "components.MovieDetails.downloadstatus": "Download Status", - "components.MovieDetails.manageModalClearMedia": "Clear All Media Data", + "components.MovieDetails.manageModalClearMedia": "Clear Media Data", "components.MovieDetails.manageModalClearMediaWarning": "* This will irreversibly remove all data for this movie, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.", "components.MovieDetails.manageModalNoRequests": "No requests.", "components.MovieDetails.manageModalRequests": "Requests", @@ -140,7 +140,7 @@ "components.PersonDetails.birthdate": "Born {birthdate}", "components.PersonDetails.crewmember": "Crew", "components.PersonDetails.lifespan": "{birthdate} – {deathdate}", - "components.PlexLoginButton.signingin": "Signing in…", + "components.PlexLoginButton.signingin": "Signing In…", "components.PlexLoginButton.signinwithplex": "Sign In", "components.QuotaSelector.movieRequestLimit": "{quotaLimit} movie(s) per {quotaDays} day(s)", "components.QuotaSelector.tvRequestLimit": "{quotaLimit} season(s) per {quotaDays} day(s)", @@ -152,16 +152,16 @@ "components.RequestBlock.rootfolder": "Root Folder", "components.RequestBlock.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", "components.RequestBlock.server": "Destination Server", - "components.RequestButton.approve4krequests": "Approve {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}", + "components.RequestButton.approve4krequests": "Approve {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}", "components.RequestButton.approverequest": "Approve Request", "components.RequestButton.approverequest4k": "Approve 4K Request", - "components.RequestButton.approverequests": "Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}", - "components.RequestButton.decline4krequests": "Decline {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}", + "components.RequestButton.approverequests": "Approve {requestCount, plural, one {Request} other {{requestCount} Requests}}", + "components.RequestButton.decline4krequests": "Decline {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}", "components.RequestButton.declinerequest": "Decline Request", "components.RequestButton.declinerequest4k": "Decline 4K Request", - "components.RequestButton.declinerequests": "Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}", + "components.RequestButton.declinerequests": "Decline {requestCount, plural, one {Request} other {{requestCount} Requests}}", "components.RequestButton.requestmore": "Request More", - "components.RequestButton.requestmore4k": "Request More 4K", + "components.RequestButton.requestmore4k": "Request More in 4K", "components.RequestButton.viewrequest": "View Request", "components.RequestButton.viewrequest4k": "View 4K Request", "components.RequestCard.deleterequest": "Delete Request", @@ -169,6 +169,7 @@ "components.RequestCard.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", "components.RequestList.RequestItem.cancelRequest": "Cancel Request", "components.RequestList.RequestItem.deleterequest": "Delete Request", + "components.RequestList.RequestItem.editrequest": "Edit Request", "components.RequestList.RequestItem.failedretry": "Something went wrong while retrying the request.", "components.RequestList.RequestItem.mediaerror": "The associated title for this request is no longer available.", "components.RequestList.RequestItem.modified": "Modified", @@ -208,13 +209,13 @@ "components.RequestModal.alreadyrequested": "Already Requested", "components.RequestModal.autoapproval": "Automatic Approval", "components.RequestModal.cancel": "Cancel Request", + "components.RequestModal.edit": "Edit Request", "components.RequestModal.errorediting": "Something went wrong while editing the request.", "components.RequestModal.extras": "Extras", "components.RequestModal.numberofepisodes": "# of Episodes", - "components.RequestModal.pending4krequest": "Pending Request for {title} in 4K", + "components.RequestModal.pending4krequest": "Pending 4K Request for {title}", "components.RequestModal.pendingapproval": "Your request is pending approval.", "components.RequestModal.pendingrequest": "Pending Request for {title}", - "components.RequestModal.request4kfrom": "There is currently a pending 4K request from {username}.", "components.RequestModal.request4ktitle": "Request {title} in 4K", "components.RequestModal.requestCancel": "Request for {title} canceled.", "components.RequestModal.requestSuccess": "{title} requested successfully!", @@ -223,7 +224,7 @@ "components.RequestModal.requestcancelled": "Request for {title} canceled.", "components.RequestModal.requestedited": "Request for {title} edited successfully!", "components.RequestModal.requesterror": "Something went wrong while submitting the request.", - "components.RequestModal.requestfrom": "There is currently a pending request from {username}.", + "components.RequestModal.requestfrom": "{username}'s request is pending approval.", "components.RequestModal.requestseasons": "Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}", "components.RequestModal.requesttitle": "Request {title}", "components.RequestModal.season": "Season", @@ -290,11 +291,13 @@ "components.Settings.Notifications.chatId": "Chat ID", "components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.", "components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved successfully!", + "components.Settings.Notifications.discordtestsent": "Discord test notification sent!", "components.Settings.Notifications.emailNotificationTypesAlertDescription": "Media Requested, Media Automatically Approved, and Media Failed email notifications are sent to all users with the Manage Requests permission.", "components.Settings.Notifications.emailNotificationTypesAlertDescriptionPt2": "Media Approved, Media Declined, and Media Available email notifications are sent to the user who submitted the request.", "components.Settings.Notifications.emailsender": "Sender Address", "components.Settings.Notifications.emailsettingsfailed": "Email notification settings failed to save.", "components.Settings.Notifications.emailsettingssaved": "Email notification settings saved successfully!", + "components.Settings.Notifications.emailtestsent": "Email test notification sent!", "components.Settings.Notifications.enableSsl": "Enable SSL", "components.Settings.Notifications.pgpPassword": "PGP Password", "components.Settings.Notifications.pgpPasswordTip": "Sign encrypted email messages using OpenPGP", @@ -309,7 +312,7 @@ "components.Settings.Notifications.ssldisabletip": "SSL should be disabled on standard TLS connections (port 587)", "components.Settings.Notifications.telegramsettingsfailed": "Telegram notification settings failed to save.", "components.Settings.Notifications.telegramsettingssaved": "Telegram notification settings saved successfully!", - "components.Settings.Notifications.testsent": "Telegram test notification sent!", + "components.Settings.Notifications.telegramtestsent": "Telegram test notification sent!", "components.Settings.Notifications.validationBotAPIRequired": "You must provide a bot authentication token", "components.Settings.Notifications.validationChatIdRequired": "You must provide a valid chat ID", "components.Settings.Notifications.validationEmail": "You must provide a valid email address", @@ -331,6 +334,7 @@ "components.Settings.RadarrModal.defaultserver": "Default Server", "components.Settings.RadarrModal.edit4kradarr": "Edit 4K Radarr Server", "components.Settings.RadarrModal.editradarr": "Edit Radarr Server", + "components.Settings.RadarrModal.enableSearch": "Enable Automatic Search", "components.Settings.RadarrModal.externalUrl": "External URL", "components.Settings.RadarrModal.externalUrlPlaceholder": "External URL pointing to your Radarr server", "components.Settings.RadarrModal.hostname": "Hostname or IP Address", @@ -340,7 +344,6 @@ "components.Settings.RadarrModal.minimumAvailability": "Minimum Availability", "components.Settings.RadarrModal.notagoptions": "No tags.", "components.Settings.RadarrModal.port": "Port", - "components.Settings.RadarrModal.preventSearch": "Disable Auto-Search", "components.Settings.RadarrModal.qualityprofile": "Quality Profile", "components.Settings.RadarrModal.rootfolder": "Root Folder", "components.Settings.RadarrModal.selectMinimumAvailability": "Select minimum availability", @@ -461,6 +464,7 @@ "components.Settings.SonarrModal.defaultserver": "Default Server", "components.Settings.SonarrModal.edit4ksonarr": "Edit 4K Sonarr Server", "components.Settings.SonarrModal.editsonarr": "Edit Sonarr Server", + "components.Settings.SonarrModal.enableSearch": "Enable Automatic Search", "components.Settings.SonarrModal.externalUrl": "External URL", "components.Settings.SonarrModal.externalUrlPlaceholder": "External URL pointing to your Sonarr server", "components.Settings.SonarrModal.hostname": "Hostname or IP Address", @@ -471,7 +475,6 @@ "components.Settings.SonarrModal.loadingrootfolders": "Loading root folders…", "components.Settings.SonarrModal.notagoptions": "No tags.", "components.Settings.SonarrModal.port": "Port", - "components.Settings.SonarrModal.preventSearch": "Disable Auto-Search", "components.Settings.SonarrModal.qualityprofile": "Quality Profile", "components.Settings.SonarrModal.rootfolder": "Root Folder", "components.Settings.SonarrModal.seasonfolders": "Season Folders", @@ -560,9 +563,9 @@ "components.Settings.regionTip": "Filter content by regional availability", "components.Settings.scan": "Sync Libraries", "components.Settings.scanning": "Syncing…", - "components.Settings.serverConnected": "connected", "components.Settings.serverLocal": "local", "components.Settings.serverRemote": "remote", + "components.Settings.serverSecure": "secure", "components.Settings.servername": "Server Name", "components.Settings.servernamePlaceholder": "Plex Server Name", "components.Settings.servernameTip": "Automatically retrieved from Plex after saving", @@ -620,7 +623,7 @@ "components.TvDetails.episodeRuntime": "Episode Runtime", "components.TvDetails.episodeRuntimeMinutes": "{runtime} minutes", "components.TvDetails.firstAirDate": "First Air Date", - "components.TvDetails.manageModalClearMedia": "Clear All Media Data", + "components.TvDetails.manageModalClearMedia": "Clear Media Data", "components.TvDetails.manageModalClearMediaWarning": "* This will irreversibly remove all data for this TV series, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.", "components.TvDetails.manageModalNoRequests": "No requests.", "components.TvDetails.manageModalRequests": "Requests", @@ -675,6 +678,7 @@ "components.UserList.totalrequests": "Total Requests", "components.UserList.user": "User", "components.UserList.usercreatedfailed": "Something went wrong while creating the user.", + "components.UserList.usercreatedfailedexisting": "The provided email address is already in use by another user.", "components.UserList.usercreatedsuccess": "User created successfully!", "components.UserList.userdeleted": "User deleted successfully!", "components.UserList.userdeleteerror": "Something went wrong while deleting the user.", @@ -794,11 +798,12 @@ "i18n.previous": "Previous", "i18n.processing": "Processing", "i18n.request": "Request", - "i18n.request4k": "Request 4K", + "i18n.request4k": "Request in 4K", "i18n.requested": "Requested", "i18n.requesting": "Requesting…", "i18n.resultsperpage": "Display {pageSize} results per page", "i18n.retry": "Retry", + "i18n.retrying": "Retrying…", "i18n.save": "Save Changes", "i18n.saving": "Saving…", "i18n.settings": "Settings", diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 900f17af..da750750 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,3 +1,4 @@ +import { ArrowCircleRightIcon } from '@heroicons/react/outline'; import Link from 'next/link'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -22,22 +23,9 @@ const Custom404: React.FC = () => { })} - + {intl.formatMessage(messages.returnHome)} - - - + diff --git a/src/pages/_error.tsx b/src/pages/_error.tsx index bb24b57f..c1960638 100644 --- a/src/pages/_error.tsx +++ b/src/pages/_error.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import { ArrowCircleRightIcon } from '@heroicons/react/outline'; import type { NextPage } from 'next'; import Link from 'next/link'; -import type { Undefinable } from '../utils/typeHelpers'; +import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import PageTitle from '../components/Common/PageTitle'; +import type { Undefinable } from '../utils/typeHelpers'; interface ErrorProps { statusCode?: number; @@ -45,22 +46,9 @@ const Error: NextPage = ({ statusCode }) => { : getErrorMessage(statusCode)} - + {intl.formatMessage(messages.returnHome)} - - - + diff --git a/yarn.lock b/yarn.lock index 91f368f1..333bc243 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1533,6 +1533,11 @@ resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.0.0.tgz#661b50ebfd25041abb45d8eedd85e7559056bcaf" integrity sha512-mjqRJrgkbcHQBfAHnqH0yRxO/y/22jYrdltpE7WkurafREKZ+pj5bPBwYHMt935Sdz/n16yRcVmsSCqDFHee9A== +"@heroicons/react@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.1.tgz#66d25f6441920bd5c2146ea27fd33995885452dd" + integrity sha512-uikw2gKCmqnvjVxitecWfFLMOKyL9BTFcU4VM3hHj9OMwpkCr5Ke+MRMyY2/aQVmsYs4VTq7NCFX05MYwAHi3g== + "@iarna/cli@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@iarna/cli/-/cli-1.2.0.tgz#0f7af5e851afe895104583c4ca07377a8094d641"