Merge branch 'develop'

pull/2563/head
sct 3 years ago
commit 24095eac03
No known key found for this signature in database
GPG Key ID: 77D146606D30DCCD

@ -458,6 +458,15 @@
"contributions": [ "contributions": [
"translation" "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": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>", "badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",

@ -12,7 +12,7 @@
<a href="https://lgtm.com/projects/g/sct/overseerr/context:javascript"><img alt="Language grade: JavaScript" src="https://img.shields.io/lgtm/grade/javascript/g/sct/overseerr.svg?logo=lgtm&logoWidth=18"/></a> <a href="https://lgtm.com/projects/g/sct/overseerr/context:javascript"><img alt="Language grade: JavaScript" src="https://img.shields.io/lgtm/grade/javascript/g/sct/overseerr.svg?logo=lgtm&logoWidth=18"/></a>
<a href="https://github.com/sct/overseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/sct/overseerr"></a> <a href="https://github.com/sct/overseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/sct/overseerr"></a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-49-orange.svg"/></a> <a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-50-orange.svg"/></a>
<!-- ALL-CONTRIBUTORS-BADGE:END --> <!-- ALL-CONTRIBUTORS-BADGE:END -->
</p> </p>
@ -137,6 +137,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://nz.linkedin.com/in/jonocairns"><img src="https://avatars.githubusercontent.com/u/182836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jono Cairns</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=jonocairns" title="Code">💻</a></td> <td align="center"><a href="https://nz.linkedin.com/in/jonocairns"><img src="https://avatars.githubusercontent.com/u/182836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jono Cairns</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=jonocairns" title="Code">💻</a></td>
<td align="center"><a href="https://scias.net/"><img src="https://avatars.githubusercontent.com/u/439655?v=4?s=100" width="100px;" alt=""/><br /><sub><b>DJScias</b></sub></a><br /><a href="#translation-DJScias" title="Translation">🌍</a></td> <td align="center"><a href="https://scias.net/"><img src="https://avatars.githubusercontent.com/u/439655?v=4?s=100" width="100px;" alt=""/><br /><sub><b>DJScias</b></sub></a><br /><a href="#translation-DJScias" title="Translation">🌍</a></td>
</tr> </tr>
<tr>
<td align="center"><a href="https://github.com/Dabu-dot"><img src="https://avatars.githubusercontent.com/u/52525576?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dabu-dot</b></sub></a><br /><a href="#translation-Dabu-dot" title="Translation">🌍</a></td>
</tr>
</table> </table>
<!-- markdownlint-restore --> <!-- markdownlint-restore -->

@ -178,11 +178,11 @@ If the hostname or IP address you configured above is not accessible outside you
#### Enable Scan #### 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 ## Notifications

@ -18,6 +18,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@headlessui/react": "^1.0.0", "@headlessui/react": "^1.0.0",
"@heroicons/react": "^1.0.1",
"@supercharge/request-ip": "^1.1.2", "@supercharge/request-ip": "^1.1.2",
"@svgr/webpack": "^5.5.0", "@svgr/webpack": "^5.5.0",
"@tanem/react-nprogress": "^3.0.62", "@tanem/react-nprogress": "^3.0.62",

@ -72,7 +72,8 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
} catch (e) { } catch (e) {
logger.error('Error retrieving movie by TMDb ID', { logger.error('Error retrieving movie by TMDb ID', {
label: 'Radarr API', label: 'Radarr API',
message: e.message, errorMessage: e.message,
tmdbId: id,
}); });
throw new Error('Movie not found'); 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', 'Title already exists and is available. Skipping add and returning success',
{ {
label: 'Radarr', label: 'Radarr',
movie,
} }
); );
return 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) { if (movie.id && !movie.monitored) {
const response = await this.axios.put<RadarrMovie>(`/movie`, { const response = await this.axios.put<RadarrMovie>(`/movie`, {
...movie, ...movie,
@ -115,16 +117,25 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
if (response.data.monitored) { if (response.data.monitored) {
logger.info( logger.info(
'Found existing title in Radarr and set it to monitored. Returning success', 'Found existing title in Radarr and set it to monitored.',
{ label: 'Radarr' } {
label: 'Radarr',
movieId: response.data.id,
movieTitle: response.data.title,
}
); );
logger.debug('Radarr update details', { logger.debug('Radarr update details', {
label: 'Radarr', label: 'Radarr',
movie: response.data, movie: response.data,
}); });
if (options.searchNow) {
this.searchMovie(response.data.id);
}
return response.data; return response.data;
} else { } else {
logger.error('Failed to update existing movie in Radarr', { logger.error('Failed to update existing movie in Radarr.', {
label: 'Radarr', label: 'Radarr',
options, options,
}); });
@ -183,6 +194,26 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
throw new Error('Failed to add movie to Radarr'); throw new Error('Failed to add movie to Radarr');
} }
}; };
public async searchMovie(movieId: number): Promise<void> {
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; export default RadarrAPI;

@ -113,7 +113,8 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
} catch (e) { } catch (e) {
logger.error('Error retrieving series by series title', { logger.error('Error retrieving series by series title', {
label: 'Sonarr API', label: 'Sonarr API',
message: e.message, errorMessage: e.message,
title,
}); });
throw new Error('No series found'); throw new Error('No series found');
} }
@ -135,7 +136,8 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
} catch (e) { } catch (e) {
logger.error('Error retrieving series by tvdb ID', { logger.error('Error retrieving series by tvdb ID', {
label: 'Sonarr API', label: 'Sonarr API',
message: e.message, errorMessage: e.message,
tvdbId: id,
}); });
throw new Error('Series not found'); throw new Error('Series not found');
} }
@ -156,16 +158,21 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
); );
if (newSeriesResponse.data.id) { if (newSeriesResponse.data.id) {
logger.info('Sonarr accepted request. Updated existing series', { logger.info('Updated existing series in Sonarr.', {
label: 'Sonarr', label: 'Sonarr',
seriesId: newSeriesResponse.data.id,
seriesTitle: newSeriesResponse.data.title,
}); });
logger.debug('Sonarr update details', { logger.debug('Sonarr update details', {
label: 'Sonarr', label: 'Sonarr',
movie: newSeriesResponse.data, movie: newSeriesResponse.data,
}); });
if (options.searchNow) { if (options.searchNow) {
this.searchSeries(newSeriesResponse.data.id); this.searchSeries(newSeriesResponse.data.id);
} }
return newSeriesResponse.data;
} else { } else {
logger.error('Failed to update series in Sonarr', { logger.error('Failed to update series in Sonarr', {
label: 'Sonarr', label: 'Sonarr',
@ -173,8 +180,6 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
}); });
throw new Error('Failed to update series in Sonarr'); throw new Error('Failed to update series in Sonarr');
} }
return newSeriesResponse.data;
} }
const createdSeriesResponse = await this.axios.post<SonarrSeries>( const createdSeriesResponse = await this.axios.post<SonarrSeries>(
@ -223,7 +228,7 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
logger.error('Something went wrong while adding a series to Sonarr.', { logger.error('Something went wrong while adding a series to Sonarr.', {
label: 'Sonarr API', label: 'Sonarr API',
errorMessage: e.message, errorMessage: e.message,
error: e, options,
response: e?.response?.data, response: e?.response?.data,
}); });
throw new Error('Failed to add series'); 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.', 'Something went wrong while retrieving Sonarr language profiles.',
{ {
label: 'Sonarr API', 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<void> { public async searchSeries(seriesId: number): Promise<void> {
logger.info('Executing series search command', { logger.info('Executing series search command.', {
label: 'Sonarr API', label: 'Sonarr API',
seriesId, 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( private buildSeasonList(

@ -142,7 +142,9 @@ export class MediaRequest {
if (this.type === MediaType.MOVIE) { if (this.type === MediaType.MOVIE) {
const movie = await tmdb.getMovie({ movieId: media.tmdbId }); const movie = await tmdb.getMovie({ movieId: media.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_PENDING, { notificationManager.sendNotification(Notification.MEDIA_PENDING, {
subject: movie.title, subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: movie.overview, message: movie.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
media, media,
@ -153,7 +155,9 @@ export class MediaRequest {
if (this.type === MediaType.TV) { if (this.type === MediaType.TV) {
const tv = await tmdb.getTvShow({ tvId: media.tmdbId }); const tv = await tmdb.getTvShow({ tvId: media.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_PENDING, { 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, message: tv.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
media, media,
@ -210,7 +214,9 @@ export class MediaRequest {
: Notification.MEDIA_APPROVED : Notification.MEDIA_APPROVED
: Notification.MEDIA_DECLINED, : Notification.MEDIA_DECLINED,
{ {
subject: movie.title, subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: movie.overview, message: movie.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
notifyUser: autoApproved ? undefined : this.requestedBy, notifyUser: autoApproved ? undefined : this.requestedBy,
@ -227,7 +233,9 @@ export class MediaRequest {
: Notification.MEDIA_APPROVED : Notification.MEDIA_APPROVED
: Notification.MEDIA_DECLINED, : Notification.MEDIA_DECLINED,
{ {
subject: tv.name, subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: tv.overview, message: tv.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
notifyUser: autoApproved ? undefined : this.requestedBy, notifyUser: autoApproved ? undefined : this.requestedBy,
@ -492,7 +500,9 @@ export class MediaRequest {
); );
notificationManager.sendNotification(Notification.MEDIA_FAILED, { notificationManager.sendNotification(Notification.MEDIA_FAILED, {
subject: movie.title, subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: movie.overview, message: movie.overview,
media, media,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, 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, { 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, message: series.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${series.poster_path}`, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${series.poster_path}`,
media, media,

@ -1,5 +1,4 @@
import logger from '../../logger'; import logger from '../../logger';
import { getSettings } from '../settings';
import type { NotificationAgent, NotificationPayload } from './agents/agent'; import type { NotificationAgent, NotificationPayload } from './agents/agent';
export enum Notification { export enum Notification {
@ -45,13 +44,13 @@ class NotificationManager {
type: Notification, type: Notification,
payload: NotificationPayload payload: NotificationPayload
): void { ): void {
const settings = getSettings().notifications;
logger.info(`Sending notification(s) for ${Notification[type]}`, { logger.info(`Sending notification(s) for ${Notification[type]}`, {
label: 'Notifications', label: 'Notifications',
subject: payload.subject, subject: payload.subject,
}); });
this.activeAgents.forEach((agent) => { this.activeAgents.forEach((agent) => {
if (settings.enabled && agent.shouldSend(type)) { if (agent.shouldSend(type)) {
agent.send(type, payload); agent.send(type, payload);
} }
}); });

@ -179,7 +179,6 @@ interface NotificationAgents {
} }
interface NotificationSettings { interface NotificationSettings {
enabled: boolean;
agents: NotificationAgents; agents: NotificationAgents;
} }
@ -234,7 +233,6 @@ class Settings {
initialized: false, initialized: false,
}, },
notifications: { notifications: {
enabled: true,
agents: { agents: {
email: { email: {
enabled: false, enabled: false,

@ -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(); const quotas = await requestUser.getQuota();
if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) { if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
@ -463,7 +493,6 @@ requestRoutes.get('/:requestId', async (req, res, next) => {
requestRoutes.put<{ requestId: string }>( requestRoutes.put<{ requestId: string }>(
'/:requestId', '/:requestId',
isAuthenticated(Permission.MANAGE_REQUESTS),
async (req, res, next) => { async (req, res, next) => {
const requestRepository = getRepository(MediaRequest); const requestRepository = getRepository(MediaRequest);
const userRepository = getRepository(User); const userRepository = getRepository(User);
@ -473,17 +502,30 @@ requestRoutes.put<{ requestId: string }>(
); );
if (!request) { 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; let requestUser = req.user;
if ( if (
req.body.userId && req.body.userId &&
!( req.body.userId !== req.user?.id &&
req.user?.hasPermission(Permission.MANAGE_USERS) && !req.user?.hasPermission([
req.user?.hasPermission(Permission.MANAGE_REQUESTS) Permission.MANAGE_USERS,
) Permission.MANAGE_REQUESTS,
])
) { ) {
return next({ return next({
status: 403, status: 403,
@ -516,7 +558,7 @@ requestRoutes.put<{ requestId: string }>(
if (!requestedSeasons || requestedSeasons.length === 0) { if (!requestedSeasons || requestedSeasons.length === 0) {
throw new Error( 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({ return next({
status: 401, 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(); return res.status(204).send();
} catch (e) { } catch (e) {
logger.error(e.message); 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', label: 'Media Request',
message: e.message, 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', label: 'Media Request',
message: e.message, message: e.message,
}); });
next({ status: 404, message: 'Request not found' }); next({ status: 404, message: 'Request not found.' });
} }
} }
); );

@ -4,11 +4,13 @@ import fs from 'fs';
import { merge, omit } from 'lodash'; import { merge, omit } from 'lodash';
import path from 'path'; import path from 'path';
import { getRepository } from 'typeorm'; import { getRepository } from 'typeorm';
import { URL } from 'url';
import PlexAPI from '../../api/plexapi'; import PlexAPI from '../../api/plexapi';
import PlexTvAPI from '../../api/plextv'; import PlexTvAPI from '../../api/plextv';
import Media from '../../entity/Media'; import Media from '../../entity/Media';
import { MediaRequest } from '../../entity/MediaRequest'; import { MediaRequest } from '../../entity/MediaRequest';
import { User } from '../../entity/User'; import { User } from '../../entity/User';
import { PlexConnection } from '../../interfaces/api/plexInterfaces';
import { import {
LogMessage, LogMessage,
LogsResultsResponse, LogsResultsResponse,
@ -129,13 +131,32 @@ settingsRoutes.get('/plex/devices/servers', async (req, res, next) => {
if (devices) { if (devices) {
await Promise.all( await Promise.all(
devices.map(async (device) => { 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( await Promise.all(
device.connection.map(async (connection) => { device.connection.map(async (connection) => {
const plexDeviceSettings = { const plexDeviceSettings = {
...settings.plex, ...settings.plex,
ip: connection.address, ip: connection.address,
port: connection.port, port: connection.port,
useSsl: !connection.local && connection.protocol === 'https', useSsl: connection.protocol === 'https',
}; };
const plexClient = new PlexAPI({ const plexClient = new PlexAPI({
plexToken: admin.plexToken, plexToken: admin.plexToken,
@ -149,7 +170,7 @@ settingsRoutes.get('/plex/devices/servers', async (req, res, next) => {
connection.message = 'OK'; connection.message = 'OK';
} catch (e) { } catch (e) {
connection.status = 500; connection.status = 500;
connection.message = e.message; connection.message = e.message.split(':')[0];
} }
}) })
); );

@ -81,10 +81,25 @@ router.post(
const body = req.body; const body = req.body;
const userRepository = getRepository(User); 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 passedExplicitPassword = body.password && body.password.length > 0;
const avatar = gravatarUrl(body.email, { default: 'mm', size: 200 }); 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'); throw new Error('Email notifications must be enabled');
} }

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" fill="none" viewBox="0 0 44 44"><path fill="#31C48D" d="M43.5 0H0L43.5 43.5V0Z"/><path fill="#F7FAFC" fill-rule="evenodd" d="M36.707 9.29303C36.8945 9.48056 36.9998 9.73487 36.9998 10C36.9998 10.2652 36.8945 10.5195 36.707 10.707L28.707 18.707C28.5195 18.8945 28.2652 18.9998 28 18.9998C27.7348 18.9998 27.4805 18.8945 27.293 18.707L23.293 14.707C23.1108 14.5184 23.0101 14.2658 23.0123 14.0036C23.0146 13.7414 23.1198 13.4906 23.3052 13.3052C23.4906 13.1198 23.7414 13.0146 24.0036 13.0124C24.2658 13.0101 24.5184 13.1109 24.707 13.293L28 16.586L35.293 9.29303C35.4805 9.10556 35.7348 9.00024 36 9.00024C36.2652 9.00024 36.5195 9.10556 36.707 9.29303Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 744 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 250 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 319 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" fill="none" viewBox="0 0 44 44"><path fill="#ED8936" d="M43.5 0H0L43.5 43.5V0Z"/><path fill="#fff" d="M31 5.79999C29.5678 5.79999 28.1943 6.36891 27.1816 7.38161C26.1689 8.39431 25.6 9.76782 25.6 11.2V14.4274L24.9637 15.0637C24.8379 15.1896 24.7522 15.3499 24.7175 15.5245C24.6828 15.699 24.7006 15.8799 24.7687 16.0444C24.8368 16.2088 24.9521 16.3494 25.1001 16.4482C25.2481 16.5471 25.422 16.5999 25.6 16.6H36.4C36.578 16.5999 36.7519 16.5471 36.8999 16.4482C37.0479 16.3494 37.1632 16.2088 37.2313 16.0444C37.2994 15.8799 37.3172 15.699 37.2825 15.5245C37.2478 15.3499 37.1621 15.1896 37.0363 15.0637L36.4 14.4274V11.2C36.4 9.76782 35.8311 8.39431 34.8184 7.38161C33.8057 6.36891 32.4322 5.79999 31 5.79999ZM31 20.2C30.2839 20.2 29.5972 19.9155 29.0908 19.4092C28.5845 18.9028 28.3 18.2161 28.3 17.5H33.7C33.7 18.2161 33.4155 18.9028 32.9092 19.4092C32.4028 19.9155 31.7161 20.2 31 20.2Z"/></svg>

Before

Width:  |  Height:  |  Size: 962 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" fill="none" viewBox="0 0 44 44"><path fill="#667EEA" d="M43.5 0H0L43.5 43.5V0Z"/><path fill="#fff" fill-rule="evenodd" d="M31 20.2C32.9096 20.2 34.7409 19.4414 36.0912 18.0912C37.4414 16.7409 38.2 14.9095 38.2 13C38.2 11.0904 37.4414 9.25908 36.0912 7.90882C34.7409 6.55856 32.9096 5.79999 31 5.79999C29.0904 5.79999 27.2591 6.55856 25.9088 7.90882C24.5586 9.25908 23.8 11.0904 23.8 13C23.8 14.9095 24.5586 16.7409 25.9088 18.0912C27.2591 19.4414 29.0904 20.2 31 20.2ZM31.9 9.39999C31.9 9.16129 31.8052 8.93237 31.6364 8.76359C31.4676 8.59481 31.2387 8.49999 31 8.49999C30.7613 8.49999 30.5324 8.59481 30.3636 8.76359C30.1948 8.93237 30.1 9.16129 30.1 9.39999V13C30.1 13.2387 30.1949 13.4675 30.3637 13.6363L32.9089 16.1824C32.9925 16.266 33.0918 16.3323 33.201 16.3776C33.3103 16.4228 33.4274 16.4461 33.5456 16.4461C33.6639 16.4461 33.781 16.4228 33.8903 16.3776C33.9995 16.3323 34.0988 16.266 34.1824 16.1824C34.266 16.0988 34.3323 15.9995 34.3776 15.8902C34.4229 15.781 34.4461 15.6639 34.4461 15.5456C34.4461 15.4274 34.4229 15.3103 34.3776 15.201C34.3323 15.0918 34.266 14.9925 34.1824 14.9089L31.9 12.6274V9.39999Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="currentColor" class="w-6 h-6" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"/></svg>

Before

Width:  |  Height:  |  Size: 284 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" class="w-6 h-6" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>

Before

Width:  |  Height:  |  Size: 260 B

@ -1,3 +1,4 @@
import { DownloadIcon, DuplicateIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import { uniq } from 'lodash'; import { uniq } from 'lodash';
import Link from 'next/link'; import Link from 'next/link';
@ -248,22 +249,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
title={intl.formatMessage( title={intl.formatMessage(
is4k ? messages.requestcollection4k : messages.requestcollection is4k ? messages.requestcollection4k : messages.requestcollection
)} )}
iconSvg={ iconSvg={<DuplicateIcon className="w-6 h-6" />}
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
}
> >
<p> <p>
{intl.formatMessage( {intl.formatMessage(
@ -355,20 +341,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
}} }}
text={ text={
<> <>
<svg <DownloadIcon className="w-5 h-5 mr-1" />
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
<span> <span>
{intl.formatMessage( {intl.formatMessage(
hasRequestable hasRequestable
@ -393,20 +366,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
setIs4k(true); setIs4k(true);
}} }}
> >
<svg <DownloadIcon className="w-5 h-5 mr-1" />
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
<span> <span>
{intl.formatMessage(messages.requestcollection4k)} {intl.formatMessage(messages.requestcollection4k)}
</span> </span>

@ -1,3 +1,8 @@
import {
ExclamationIcon,
InformationCircleIcon,
XCircleIcon,
} from '@heroicons/react/solid';
import React from 'react'; import React from 'react';
interface AlertProps { interface AlertProps {
@ -10,21 +15,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
bgColor: 'bg-yellow-600', bgColor: 'bg-yellow-600',
titleColor: 'text-yellow-200', titleColor: 'text-yellow-200',
textColor: 'text-yellow-300', textColor: 'text-yellow-300',
svg: ( svg: <ExclamationIcon className="w-5 h-5" />,
<svg
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
),
}; };
switch (type) { switch (type) {
@ -33,22 +24,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
bgColor: 'bg-indigo-600', bgColor: 'bg-indigo-600',
titleColor: 'text-indigo-200', titleColor: 'text-indigo-200',
textColor: 'text-indigo-300', textColor: 'text-indigo-300',
svg: ( svg: <InformationCircleIcon className="w-5 h-5" />,
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
),
}; };
break; break;
case 'error': case 'error':
@ -56,22 +32,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
bgColor: 'bg-red-600', bgColor: 'bg-red-600',
titleColor: 'text-red-200', titleColor: 'text-red-200',
textColor: 'text-red-300', textColor: 'text-red-300',
svg: ( svg: <XCircleIcon className="w-5 h-5" />,
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
),
}; };
break; break;
} }

@ -1,13 +1,14 @@
import { ChevronDownIcon } from '@heroicons/react/solid';
import React, { import React, {
useState,
useRef,
AnchorHTMLAttributes, AnchorHTMLAttributes,
ReactNode,
ButtonHTMLAttributes, ButtonHTMLAttributes,
ReactNode,
useRef,
useState,
} from 'react'; } from 'react';
import useClickOutside from '../../../hooks/useClickOutside'; import useClickOutside from '../../../hooks/useClickOutside';
import Transition from '../../Transition';
import { withProperties } from '../../../utils/typeHelpers'; import { withProperties } from '../../../utils/typeHelpers';
import Transition from '../../Transition';
interface DropdownItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> { interface DropdownItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
buttonType?: 'primary' | 'ghost'; buttonType?: 'primary' | 'ghost';
@ -102,18 +103,7 @@ const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
{dropdownIcon ? ( {dropdownIcon ? (
dropdownIcon dropdownIcon
) : ( ) : (
<svg <ChevronDownIcon className="w-5 h-5" />
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
)} )}
</button> </button>
<Transition <Transition

@ -1,4 +1,4 @@
import React from 'react'; import React, { ReactNode } from 'react';
import ButtonWithDropdown from '../ButtonWithDropdown'; import ButtonWithDropdown from '../ButtonWithDropdown';
interface PlayButtonProps { interface PlayButtonProps {
@ -8,6 +8,7 @@ interface PlayButtonProps {
export interface PlayButtonLink { export interface PlayButtonLink {
text: string; text: string;
url: string; url: string;
svg: ReactNode;
} }
const PlayButton: React.FC<PlayButtonProps> = ({ links }) => { const PlayButton: React.FC<PlayButtonProps> = ({ links }) => {
@ -20,27 +21,8 @@ const PlayButton: React.FC<PlayButtonProps> = ({ links }) => {
buttonType="ghost" buttonType="ghost"
text={ text={
<> <>
<svg {links[0].svg}
className="w-5 h-5 mr-1" {links[0].text}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{links[0].text}</span>
</> </>
} }
onClick={() => { onClick={() => {
@ -57,6 +39,7 @@ const PlayButton: React.FC<PlayButtonProps> = ({ links }) => {
}} }}
buttonType="ghost" buttonType="ghost"
> >
{link.svg}
{link.text} {link.text}
</ButtonWithDropdown.Item> </ButtonWithDropdown.Item>
); );

@ -1,4 +1,5 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/click-events-have-key-events */
import { XIcon } from '@heroicons/react/outline';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { useLockBodyScroll } from '../../../hooks/useLockBodyScroll'; import { useLockBodyScroll } from '../../../hooks/useLockBodyScroll';
@ -81,20 +82,7 @@ const SlideOver: React.FC<SlideOverProps> = ({
className="text-indigo-200 transition duration-150 ease-in-out hover:text-white" className="text-indigo-200 transition duration-150 ease-in-out hover:text-white"
onClick={() => onClose()} onClick={() => onClose()}
> >
<svg <XIcon className="w-6 h-6" />
className="w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button> </button>
</div> </div>
</div> </div>

@ -1,12 +1,13 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import GenreCard from '../../GenreCard';
import Slider from '../../Slider';
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces'; import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
import { LanguageContext } from '../../../context/LanguageContext'; import { LanguageContext } from '../../../context/LanguageContext';
import GenreCard from '../../GenreCard';
import Slider from '../../Slider';
import { genreColorMap } from '../constants'; import { genreColorMap } from '../constants';
import Link from 'next/link';
const messages = defineMessages({ const messages = defineMessages({
moviegenres: 'Movie Genres', moviegenres: 'Movie Genres',
@ -29,20 +30,7 @@ const MovieGenreSlider: React.FC = () => {
<Link href="/discover/movies/genres"> <Link href="/discover/movies/genres">
<a className="slider-title"> <a className="slider-title">
<span>{intl.formatMessage(messages.moviegenres)}</span> <span>{intl.formatMessage(messages.moviegenres)}</span>
<svg <ArrowCircleRightIcon className="w-6 h-6 ml-2" />
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a> </a>
</Link> </Link>
</div> </div>

@ -1,12 +1,13 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import GenreCard from '../../GenreCard';
import Slider from '../../Slider';
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces'; import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
import { LanguageContext } from '../../../context/LanguageContext'; import { LanguageContext } from '../../../context/LanguageContext';
import GenreCard from '../../GenreCard';
import Slider from '../../Slider';
import { genreColorMap } from '../constants'; import { genreColorMap } from '../constants';
import Link from 'next/link';
const messages = defineMessages({ const messages = defineMessages({
tvgenres: 'Series Genres', tvgenres: 'Series Genres',
@ -29,20 +30,7 @@ const TvGenreSlider: React.FC = () => {
<Link href="/discover/tv/genres"> <Link href="/discover/tv/genres">
<a className="slider-title"> <a className="slider-title">
<span>{intl.formatMessage(messages.tvgenres)}</span> <span>{intl.formatMessage(messages.tvgenres)}</span>
<svg <ArrowCircleRightIcon className="w-6 h-6 ml-2" />
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a> </a>
</Link> </Link>
</div> </div>

@ -1,3 +1,4 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link'; import Link from 'next/link';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@ -66,20 +67,7 @@ const Discover: React.FC = () => {
<Link href="/requests?filter=all"> <Link href="/requests?filter=all">
<a className="slider-title"> <a className="slider-title">
<span>{intl.formatMessage(messages.recentrequests)}</span> <span>{intl.formatMessage(messages.recentrequests)}</span>
<svg <ArrowCircleRightIcon className="w-6 h-6 ml-2" />
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a> </a>
</Link> </Link>
</div> </div>

@ -1,3 +1,4 @@
import { TranslateIcon } from '@heroicons/react/solid';
import React, { useContext, useRef, useState } from 'react'; import React, { useContext, useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { import {
@ -100,18 +101,7 @@ const LanguagePicker: React.FC = () => {
aria-label="Language Picker" aria-label="Language Picker"
onClick={() => setDropdownOpen(true)} onClick={() => setDropdownOpen(true)}
> >
<svg <TranslateIcon className="w-6 h-6" />
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M7 2a1 1 0 011 1v1h3a1 1 0 110 2H9.578a18.87 18.87 0 01-1.724 4.78c.29.354.596.696.914 1.026a1 1 0 11-1.44 1.389c-.188-.196-.373-.396-.554-.6a19.098 19.098 0 01-3.107 3.567 1 1 0 01-1.334-1.49 17.087 17.087 0 003.13-3.733 18.992 18.992 0 01-1.487-2.494 1 1 0 111.79-.89c.234.47.489.928.764 1.372.417-.934.752-1.913.997-2.927H3a1 1 0 110-2h3V3a1 1 0 011-1zm6 6a1 1 0 01.894.553l2.991 5.982a.869.869 0 01.02.037l.99 1.98a1 1 0 11-1.79.895L15.383 16h-4.764l-.724 1.447a1 1 0 11-1.788-.894l.99-1.98.019-.038 2.99-5.982A1 1 0 0113 8zm-1.382 6h2.764L13 11.236 11.618 14z"
clipRule="evenodd"
/>
</svg>
</button> </button>
</div> </div>
<Transition <Transition

@ -1,3 +1,4 @@
import { BellIcon } from '@heroicons/react/outline';
import React from 'react'; import React from 'react';
const Notifications: React.FC = () => { const Notifications: React.FC = () => {
@ -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" 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" aria-label="Notifications"
> >
<svg <BellIcon className="w-6 h-6" />
className="h-6 w-6"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
</button> </button>
); );
}; };

@ -1,7 +1,8 @@
import { XCircleIcon } from '@heroicons/react/outline';
import { SearchIcon } from '@heroicons/react/solid';
import React from 'react'; import React from 'react';
import useSearchInput from '../../../hooks/useSearchInput';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import ClearButton from '../../../assets/xcircle.svg'; import useSearchInput from '../../../hooks/useSearchInput';
const messages = defineMessages({ const messages = defineMessages({
searchPlaceholder: 'Search Movies & TV', searchPlaceholder: 'Search Movies & TV',
@ -18,13 +19,7 @@ const SearchInput: React.FC = () => {
</label> </label>
<div className="relative flex items-center w-full text-white focus-within:text-gray-200"> <div className="relative flex items-center w-full text-white focus-within:text-gray-200">
<div className="absolute inset-y-0 flex items-center pointer-events-none left-4"> <div className="absolute inset-y-0 flex items-center pointer-events-none left-4">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"> <SearchIcon className="w-5 h-5" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
/>
</svg>
</div> </div>
<input <input
id="search_field" id="search_field"
@ -46,7 +41,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" 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()} onClick={() => clear()}
> >
<ClearButton /> <XCircleIcon className="w-5 h-5" />
</button> </button>
)} )}
</div> </div>

@ -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 Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { ReactNode, useRef } from 'react'; import React, { ReactNode, useRef } from 'react';
@ -33,20 +40,7 @@ const SidebarLinks: SidebarLinkProps[] = [
href: '/', href: '/',
messagesKey: 'dashboard', messagesKey: 'dashboard',
svgIcon: ( svgIcon: (
<svg <SparklesIcon className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300" />
className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
/>
</svg>
), ),
activeRegExp: /^\/(discover\/?(movies|tv)?)?$/, activeRegExp: /^\/(discover\/?(movies|tv)?)?$/,
}, },
@ -54,20 +48,7 @@ const SidebarLinks: SidebarLinkProps[] = [
href: '/requests', href: '/requests',
messagesKey: 'requests', messagesKey: 'requests',
svgIcon: ( svgIcon: (
<svg <ClockIcon className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300" />
className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
), ),
activeRegExp: /^\/requests/, activeRegExp: /^\/requests/,
}, },
@ -75,14 +56,7 @@ const SidebarLinks: SidebarLinkProps[] = [
href: '/users', href: '/users',
messagesKey: 'users', messagesKey: 'users',
svgIcon: ( svgIcon: (
<svg <UsersIcon className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300" />
className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
</svg>
), ),
activeRegExp: /^\/users/, activeRegExp: /^\/users/,
requiredPermission: Permission.MANAGE_USERS, requiredPermission: Permission.MANAGE_USERS,
@ -91,26 +65,7 @@ const SidebarLinks: SidebarLinkProps[] = [
href: '/settings', href: '/settings',
messagesKey: 'settings', messagesKey: 'settings',
svgIcon: ( svgIcon: (
<svg <CogIcon className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300" />
className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
), ),
activeRegExp: /^\/settings/, activeRegExp: /^\/settings/,
requiredPermission: Permission.MANAGE_SETTINGS, requiredPermission: Permission.MANAGE_SETTINGS,
@ -157,19 +112,7 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
aria-label="Close sidebar" aria-label="Close sidebar"
onClick={() => setClosed()} onClick={() => setClosed()}
> >
<svg <XIcon className="w-6 h-6 text-white" />
className="w-6 h-6 text-white"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button> </button>
</div> </div>
<div <div

@ -1,10 +1,12 @@
import React, { useState, useRef } from 'react'; import { LogoutIcon } from '@heroicons/react/outline';
import Transition from '../../Transition'; import { CogIcon, UserIcon } from '@heroicons/react/solid';
import { useUser } from '../../../hooks/useUser';
import axios from 'axios'; import axios from 'axios';
import useClickOutside from '../../../hooks/useClickOutside';
import { defineMessages, useIntl } from 'react-intl';
import Link from 'next/link'; import Link from 'next/link';
import React, { useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useClickOutside from '../../../hooks/useClickOutside';
import { useUser } from '../../../hooks/useUser';
import Transition from '../../Transition';
const messages = defineMessages({ const messages = defineMessages({
myprofile: 'Profile', myprofile: 'Profile',
@ -65,7 +67,7 @@ const UserDropdown: React.FC = () => {
> >
<Link href={`/profile`}> <Link href={`/profile`}>
<a <a
className="block px-4 py-2 text-sm text-gray-200 transition duration-150 ease-in-out hover:bg-gray-600" className="flex items-center px-4 py-2 text-sm text-gray-200 transition duration-150 ease-in-out hover:bg-gray-600"
role="menuitem" role="menuitem"
tabIndex={0} tabIndex={0}
onKeyDown={(e) => { onKeyDown={(e) => {
@ -75,12 +77,13 @@ const UserDropdown: React.FC = () => {
}} }}
onClick={() => setDropdownOpen(false)} onClick={() => setDropdownOpen(false)}
> >
{intl.formatMessage(messages.myprofile)} <UserIcon className="inline w-5 h-5 mr-2" />
<span>{intl.formatMessage(messages.myprofile)}</span>
</a> </a>
</Link> </Link>
<Link href={`/profile/settings`}> <Link href={`/profile/settings`}>
<a <a
className="block px-4 py-2 text-sm text-gray-200 transition duration-150 ease-in-out hover:bg-gray-600" className="flex items-center px-4 py-2 text-sm text-gray-200 transition duration-150 ease-in-out hover:bg-gray-600"
role="menuitem" role="menuitem"
tabIndex={0} tabIndex={0}
onKeyDown={(e) => { onKeyDown={(e) => {
@ -90,16 +93,18 @@ const UserDropdown: React.FC = () => {
}} }}
onClick={() => setDropdownOpen(false)} onClick={() => setDropdownOpen(false)}
> >
{intl.formatMessage(messages.settings)} <CogIcon className="inline w-5 h-5 mr-2" />
<span>{intl.formatMessage(messages.settings)}</span>
</a> </a>
</Link> </Link>
<a <a
href="#" href="#"
className="block px-4 py-2 text-sm text-gray-200 transition duration-150 ease-in-out hover:bg-gray-600" className="flex items-center px-4 py-2 text-sm text-gray-200 transition duration-150 ease-in-out hover:bg-gray-600"
role="menuitem" role="menuitem"
onClick={() => logout()} onClick={() => logout()}
> >
{intl.formatMessage(messages.signout)} <LogoutIcon className="inline w-5 h-5 mr-2" />
<span>{intl.formatMessage(messages.signout)}</span>
</a> </a>
</div> </div>
</div> </div>

@ -1,3 +1,9 @@
import {
ArrowCircleUpIcon,
BeakerIcon,
CodeIcon,
ServerIcon,
} from '@heroicons/react/outline';
import Link from 'next/link'; import Link from 'next/link';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@ -51,50 +57,11 @@ const VersionStatus: React.FC<VersionStatusProps> = ({ onClick }) => {
}`} }`}
> >
{data.commitTag === 'local' ? ( {data.commitTag === 'local' ? (
<svg <CodeIcon className="w-6 h-6" />
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
/>
</svg>
) : data.version.startsWith('develop-') ? ( ) : data.version.startsWith('develop-') ? (
<svg <BeakerIcon className="w-6 h-6" />
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"
/>
</svg>
) : ( ) : (
<svg <ServerIcon className="w-6 h-6" />
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"
/>
</svg>
)} )}
<div className="flex flex-col flex-1 min-w-0 px-2 truncate last:pr-0"> <div className="flex flex-col flex-1 min-w-0 px-2 truncate last:pr-0">
<span className="font-bold">{versionStream}</span> <span className="font-bold">{versionStream}</span>
@ -114,22 +81,7 @@ const VersionStatus: React.FC<VersionStatusProps> = ({ onClick }) => {
)} )}
</span> </span>
</div> </div>
{data.updateAvailable && ( {data.updateAvailable && <ArrowCircleUpIcon className="w-6 h-6" />}
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 11l3-3m0 0l3 3m-3-3v8m0-13a9 9 0 110 18 9 9 0 010-18z"
/>
</svg>
)}
</a> </a>
</Link> </Link>
); );

@ -1,3 +1,5 @@
import { MenuAlt2Icon } from '@heroicons/react/outline';
import { InformationCircleIcon } from '@heroicons/react/solid';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@ -57,19 +59,7 @@ const Layout: React.FC = ({ children }) => {
aria-label="Open sidebar" aria-label="Open sidebar"
onClick={() => setSidebarOpen(true)} onClick={() => setSidebarOpen(true)}
> >
<svg <MenuAlt2Icon className="w-6 h-6" />
className="w-6 h-6"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h16M4 18h7"
/>
</svg>
</button> </button>
<div className="flex justify-between flex-1 pr-4 md:pr-4 md:pl-4"> <div className="flex justify-between flex-1 pr-4 md:pr-4 md:pl-4">
<SearchInput /> <SearchInput />
@ -87,18 +77,7 @@ const Layout: React.FC = ({ children }) => {
<div className="p-4 mt-6 bg-indigo-700 rounded-md"> <div className="p-4 mt-6 bg-indigo-700 rounded-md">
<div className="flex"> <div className="flex">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<svg <InformationCircleIcon className="w-5 h-5 text-white" />
className="w-5 h-5 text-white"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
</div> </div>
<div className="flex-1 ml-3 md:flex md:justify-between"> <div className="flex-1 ml-3 md:flex md:justify-between">
<p className="text-sm leading-5 text-white"> <p className="text-sm leading-5 text-white">

@ -1,3 +1,4 @@
import { LoginIcon, SupportIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import Link from 'next/link'; import Link from 'next/link';
@ -12,7 +13,7 @@ const messages = defineMessages({
validationemailrequired: 'You must provide a valid email address', validationemailrequired: 'You must provide a valid email address',
validationpasswordrequired: 'You must provide a password', validationpasswordrequired: 'You must provide a password',
loginerror: 'Something went wrong while trying to sign in.', loginerror: 'Something went wrong while trying to sign in.',
signingin: 'Signing in…', signingin: 'Signing In…',
signin: 'Sign In', signin: 'Sign In',
forgotpassword: 'Forgot Password?', forgotpassword: 'Forgot Password?',
}); });
@ -103,6 +104,7 @@ const LocalLogin: React.FC<LocalLoginProps> = ({ revalidate }) => {
<span className="inline-flex rounded-md shadow-sm"> <span className="inline-flex rounded-md shadow-sm">
<Link href="/resetpassword" passHref> <Link href="/resetpassword" passHref>
<Button as="a" buttonType="ghost"> <Button as="a" buttonType="ghost">
<SupportIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.forgotpassword)} {intl.formatMessage(messages.forgotpassword)}
</Button> </Button>
</Link> </Link>
@ -113,6 +115,7 @@ const LocalLogin: React.FC<LocalLoginProps> = ({ revalidate }) => {
type="submit" type="submit"
disabled={isSubmitting || !isValid} disabled={isSubmitting || !isValid}
> >
<LoginIcon className="w-5 h-5 mr-1" />
{isSubmitting {isSubmitting
? intl.formatMessage(messages.signingin) ? intl.formatMessage(messages.signingin)
: intl.formatMessage(messages.signin)} : intl.formatMessage(messages.signin)}

@ -1,3 +1,4 @@
import { XCircleIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import { useRouter } from 'next/dist/client/router'; import { useRouter } from 'next/dist/client/router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@ -100,19 +101,7 @@ const Login: React.FC = () => {
<div className="p-4 mb-4 bg-red-600 rounded-md"> <div className="p-4 mb-4 bg-red-600 rounded-md">
<div className="flex"> <div className="flex">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<svg <XCircleIcon className="w-5 h-5 text-red-300" />
className="w-5 h-5 text-red-300"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
</div> </div>
<div className="ml-3"> <div className="ml-3">
<h3 className="text-sm font-medium text-red-300"> <h3 className="text-sm font-medium text-red-300">

@ -1,3 +1,4 @@
import { ArrowCircleRightIcon } from '@heroicons/react/solid';
import Link from 'next/link'; import Link from 'next/link';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@ -79,18 +80,7 @@ const ShowMoreCard: React.FC<ShowMoreCardProps> = ({ url, posters }) => {
)} )}
</div> </div>
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center text-white"> <div className="absolute inset-0 z-20 flex flex-col items-center justify-center text-white">
<svg <ArrowCircleRightIcon className="w-14" />
className="w-14"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 1.414L10.586 9H7a1 1 0 100 2h3.586l-1.293 1.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414z"
clipRule="evenodd"
/>
</svg>
<div className="mt-2 font-extrabold"> <div className="mt-2 font-extrabold">
{intl.formatMessage(messages.seemore)} {intl.formatMessage(messages.seemore)}
</div> </div>

@ -1,3 +1,4 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link'; import Link from 'next/link';
import React, { useContext, useEffect } from 'react'; import React, { useContext, useEffect } from 'react';
import { useSWRInfinite } from 'swr'; import { useSWRInfinite } from 'swr';
@ -140,20 +141,7 @@ const MediaSlider: React.FC<MediaSliderProps> = ({
<Link href={linkUrl}> <Link href={linkUrl}>
<a className="slider-title"> <a className="slider-title">
<span>{title}</span> <span>{title}</span>
<svg <ArrowCircleRightIcon className="w-6 h-6 ml-2" />
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a> </a>
</Link> </Link>
) : ( ) : (

@ -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 axios from 'axios';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@ -50,7 +61,7 @@ const messages = defineMessages({
manageModalTitle: 'Manage Movie', manageModalTitle: 'Manage Movie',
manageModalRequests: 'Requests', manageModalRequests: 'Requests',
manageModalNoRequests: 'No requests.', manageModalNoRequests: 'No requests.',
manageModalClearMedia: 'Clear All Media Data', manageModalClearMedia: 'Clear Media Data',
manageModalClearMediaWarning: 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.', '* 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}}', studio: '{studioCount, plural, one {Studio} other {Studios}}',
@ -105,6 +116,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
mediaLinks.push({ mediaLinks.push({
text: intl.formatMessage(messages.playonplex), text: intl.formatMessage(messages.playonplex),
url: data.mediaInfo?.plexUrl, url: data.mediaInfo?.plexUrl,
svg: <PlayIcon className="w-5 h-5 mr-1" />,
}); });
} }
@ -117,6 +129,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
mediaLinks.push({ mediaLinks.push({
text: intl.formatMessage(messages.play4konplex), text: intl.formatMessage(messages.play4konplex),
url: data.mediaInfo?.plexUrl4k, url: data.mediaInfo?.plexUrl4k,
svg: <PlayIcon className="w-5 h-5 mr-1" />,
}); });
} }
@ -129,6 +142,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
mediaLinks.push({ mediaLinks.push({
text: intl.formatMessage(messages.watchtrailer), text: intl.formatMessage(messages.watchtrailer),
url: trailerUrl, url: trailerUrl,
svg: <FilmIcon className="w-5 h-5 mr-1" />,
}); });
} }
@ -266,18 +280,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
className="w-full sm:mb-0" className="w-full sm:mb-0"
buttonType="success" buttonType="success"
> >
<svg <CheckCircleIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z"
clipRule="evenodd"
/>
</svg>
<span>{intl.formatMessage(messages.markavailable)}</span> <span>{intl.formatMessage(messages.markavailable)}</span>
</Button> </Button>
</div> </div>
@ -291,18 +294,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
className="w-full sm:mb-0" className="w-full sm:mb-0"
buttonType="success" buttonType="success"
> >
<svg <CheckCircleIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z"
clipRule="evenodd"
/>
</svg>
<span> <span>
{intl.formatMessage(messages.mark4kavailable)} {intl.formatMessage(messages.mark4kavailable)}
</span> </span>
@ -341,15 +333,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
className="block mb-2 last:mb-0" className="block mb-2 last:mb-0"
> >
<Button buttonType="ghost" className="w-full"> <Button buttonType="ghost" className="w-full">
<svg <ExternalLinkIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
<span>{intl.formatMessage(messages.openradarr)}</span> <span>{intl.formatMessage(messages.openradarr)}</span>
</Button> </Button>
</a> </a>
@ -361,15 +345,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
rel="noreferrer" rel="noreferrer"
> >
<Button buttonType="ghost" className="w-full"> <Button buttonType="ghost" className="w-full">
<svg <ExternalLinkIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
<span>{intl.formatMessage(messages.openradarr4k)}</span> <span>{intl.formatMessage(messages.openradarr4k)}</span>
</Button> </Button>
</a> </a>
@ -383,6 +359,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
confirmText={intl.formatMessage(globalMessages.areyousure)} confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full" className="w-full"
> >
<DocumentRemoveIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.manageModalClearMedia)} {intl.formatMessage(messages.manageModalClearMedia)}
</ConfirmButton> </ConfirmButton>
<div className="mt-2 text-sm text-gray-400"> <div className="mt-2 text-sm text-gray-400">
@ -463,27 +440,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
className="ml-2 first:ml-0" className="ml-2 first:ml-0"
onClick={() => setShowManager(true)} onClick={() => setShowManager(true)}
> >
<svg <CogIcon className="w-5" />
className="w-5"
style={{ height: 18 }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</Button> </Button>
)} )}
</div> </div>
@ -513,20 +470,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
<Link href={`/movie/${data.id}/crew`}> <Link href={`/movie/${data.id}/crew`}>
<a className="flex items-center text-gray-400 transition duration-300 hover:text-gray-100"> <a className="flex items-center text-gray-400 transition duration-300 hover:text-gray-100">
<span>{intl.formatMessage(messages.viewfullcrew)}</span> <span>{intl.formatMessage(messages.viewfullcrew)}</span>
<svg <ArrowCircleRightIcon className="inline-block w-5 h-5 ml-1" />
className="inline-block w-5 h-5 ml-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a> </a>
</Link> </Link>
</div> </div>
@ -709,20 +653,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
<Link href="/movie/[movieId]/cast" as={`/movie/${data.id}/cast`}> <Link href="/movie/[movieId]/cast" as={`/movie/${data.id}/cast`}>
<a className="slider-title"> <a className="slider-title">
<span>{intl.formatMessage(messages.cast)}</span> <span>{intl.formatMessage(messages.cast)}</span>
<svg <ArrowCircleRightIcon className="w-6 h-6 ml-2" />
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a> </a>
</Link> </Link>
</div> </div>

@ -1,3 +1,4 @@
import { UserCircleIcon } from '@heroicons/react/solid';
import Link from 'next/link'; import Link from 'next/link';
import React, { useState } from 'react'; import React, { useState } from 'react';
import CachedImage from '../Common/CachedImage'; import CachedImage from '../Common/CachedImage';
@ -57,18 +58,7 @@ const PersonCard: React.FC<PersonCardProps> = ({
/> />
</div> </div>
) : ( ) : (
<svg <UserCircleIcon className="h-full" />
className="h-full"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0010 11z"
clipRule="evenodd"
/>
</svg>
)} )}
</div> </div>
<div className="w-full text-center truncate">{name}</div> <div className="w-full text-center truncate">{name}</div>

@ -1,3 +1,4 @@
import { LoginIcon } from '@heroicons/react/outline';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
@ -5,7 +6,7 @@ import PlexOAuth from '../../utils/plex';
const messages = defineMessages({ const messages = defineMessages({
signinwithplex: 'Sign In', signinwithplex: 'Sign In',
signingin: 'Signing in…', signingin: 'Signing In…',
}); });
const plexOAuth = new PlexOAuth(); const plexOAuth = new PlexOAuth();
@ -48,6 +49,7 @@ const PlexLoginButton: React.FC<PlexLoginButtonProps> = ({
disabled={loading || isProcessing} disabled={loading || isProcessing}
className="plex-button" className="plex-button"
> >
<LoginIcon className="w-5 h-5 mr-1" />
{loading {loading
? intl.formatMessage(globalMessages.loading) ? intl.formatMessage(globalMessages.loading)
: isProcessing : isProcessing

@ -1,11 +1,12 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Listbox, Transition } from '@headlessui/react'; import { Listbox, Transition } from '@headlessui/react';
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 useSWR from 'swr';
import type { Region } from '../../../server/lib/settings'; import type { Region } from '../../../server/lib/settings';
import { defineMessages, useIntl } from 'react-intl';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import { hasFlag } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css';
const messages = defineMessages({ const messages = defineMessages({
regionDefault: 'All Regions', regionDefault: 'All Regions',
@ -125,20 +126,7 @@ const RegionSelector: React.FC<RegionSelectorProps> = ({
: intl.formatMessage(messages.regionDefault)} : intl.formatMessage(messages.regionDefault)}
</span> </span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> <span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<svg <ChevronDownIcon className="w-5 h-5 text-gray-500" />
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 20 20"
className="w-5 h-5 text-gray-500"
>
<path
stroke="#6b7280"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M6 8l4 4 4-4"
/>
</svg>
</span> </span>
</Listbox.Button> </Listbox.Button>
</span> </span>
@ -196,18 +184,7 @@ const RegionSelector: React.FC<RegionSelectorProps> = ({
active ? 'text-white' : 'text-indigo-600' active ? 'text-white' : 'text-indigo-600'
} absolute inset-y-0 left-0 flex items-center pl-1.5`} } absolute inset-y-0 left-0 flex items-center pl-1.5`}
> >
<svg <CheckIcon className="w-5 h-5" />
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span> </span>
)} )}
</div> </div>
@ -234,18 +211,7 @@ const RegionSelector: React.FC<RegionSelectorProps> = ({
active ? 'text-white' : 'text-indigo-600' active ? 'text-white' : 'text-indigo-600'
} absolute inset-y-0 left-0 flex items-center pl-1.5`} } absolute inset-y-0 left-0 flex items-center pl-1.5`}
> >
<svg <CheckIcon className="w-5 h-5" />
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span> </span>
)} )}
</div> </div>
@ -286,18 +252,7 @@ const RegionSelector: React.FC<RegionSelectorProps> = ({
active ? 'text-white' : 'text-indigo-600' active ? 'text-white' : 'text-indigo-600'
} absolute inset-y-0 left-0 flex items-center pl-1.5`} } absolute inset-y-0 left-0 flex items-center pl-1.5`}
> >
<svg <CheckIcon className="w-5 h-5" />
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span> </span>
)} )}
</div> </div>

@ -1,3 +1,12 @@
import {
CalendarIcon,
CheckIcon,
EyeIcon,
PencilIcon,
TrashIcon,
UserIcon,
XIcon,
} from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@ -69,37 +78,14 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex-col items-center flex-1 min-w-0 mr-6 text-sm leading-5"> <div className="flex-col items-center flex-1 min-w-0 mr-6 text-sm leading-5">
<div className="flex mb-1 flex-nowrap white"> <div className="flex mb-1 flex-nowrap white">
<svg <UserIcon className="min-w-0 flex-shrink-0 mr-1.5 h-5 w-5" />
className="min-w-0 flex-shrink-0 mr-1.5 h-5 w-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clipRule="evenodd"
/>
</svg>
<span className="w-40 truncate md:w-auto"> <span className="w-40 truncate md:w-auto">
{request.requestedBy.displayName} {request.requestedBy.displayName}
</span> </span>
</div> </div>
{request.modifiedBy && ( {request.modifiedBy && (
<div className="flex flex-nowrap"> <div className="flex flex-nowrap">
<svg <EyeIcon className="flex-shrink-0 mr-1.5 h-5 w-5" />
className="flex-shrink-0 mr-1.5 h-5 w-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path
fillRule="evenodd"
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
clipRule="evenodd"
/>
</svg>
<span className="w-40 truncate md:w-auto"> <span className="w-40 truncate md:w-auto">
{request.modifiedBy?.displayName} {request.modifiedBy?.displayName}
</span> </span>
@ -115,18 +101,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
onClick={() => updateRequest('approve')} onClick={() => updateRequest('approve')}
disabled={isUpdating} disabled={isUpdating}
> >
<svg <CheckIcon className="w-4 h-4" />
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</Button> </Button>
</span> </span>
<span className="mr-1"> <span className="mr-1">
@ -135,18 +110,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
onClick={() => updateRequest('decline')} onClick={() => updateRequest('decline')}
disabled={isUpdating} disabled={isUpdating}
> >
<svg <XIcon className="w-4 h-4" />
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</Button> </Button>
</span> </span>
<span> <span>
@ -155,14 +119,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
onClick={() => setShowEditModal(true)} onClick={() => setShowEditModal(true)}
disabled={isUpdating} disabled={isUpdating}
> >
<svg <PencilIcon className="w-4 h-4" />
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</Button> </Button>
</span> </span>
</> </>
@ -173,18 +130,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
onClick={() => deleteRequest()} onClick={() => deleteRequest()}
disabled={isUpdating} disabled={isUpdating}
> >
<svg <TrashIcon className="w-4 h-4" />
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
</Button> </Button>
)} )}
</div> </div>
@ -215,18 +161,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
</div> </div>
</div> </div>
<div className="flex items-center mt-2 text-sm leading-5 sm:mt-0"> <div className="flex items-center mt-2 text-sm leading-5 sm:mt-0">
<svg <CalendarIcon className="flex-shrink-0 mr-1.5 h-5 w-5" />
className="flex-shrink-0 mr-1.5 h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
clipRule="evenodd"
/>
</svg>
<span> <span>
{intl.formatDate(request.createdAt, { {intl.formatDate(request.createdAt, {
year: 'numeric', year: 'numeric',

@ -1,5 +1,11 @@
import { DownloadIcon } from '@heroicons/react/outline';
import {
CheckIcon,
InformationCircleIcon,
XIcon,
} from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { import {
MediaRequestStatus, MediaRequestStatus,
@ -17,19 +23,19 @@ const messages = defineMessages({
viewrequest: 'View Request', viewrequest: 'View Request',
viewrequest4k: 'View 4K Request', viewrequest4k: 'View 4K Request',
requestmore: 'Request More', requestmore: 'Request More',
requestmore4k: 'Request More 4K', requestmore4k: 'Request More in 4K',
approverequest: 'Approve Request', approverequest: 'Approve Request',
approverequest4k: 'Approve 4K Request', approverequest4k: 'Approve 4K Request',
declinerequest: 'Decline Request', declinerequest: 'Decline Request',
declinerequest4k: 'Decline 4K Request', declinerequest4k: 'Decline 4K Request',
approverequests: approverequests:
'Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}', 'Approve {requestCount, plural, one {Request} other {{requestCount} Requests}}',
declinerequests: declinerequests:
'Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}', 'Decline {requestCount, plural, one {Request} other {{requestCount} Requests}}',
approve4krequests: approve4krequests:
'Approve {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}', 'Approve {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}',
decline4krequests: decline4krequests:
'Decline {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}', 'Decline {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}',
}); });
interface ButtonOption { interface ButtonOption {
@ -58,26 +64,34 @@ const RequestButton: React.FC<RequestButtonProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const settings = useSettings(); const settings = useSettings();
const { hasPermission } = useUser(); const { user, hasPermission } = useUser();
const [showRequestModal, setShowRequestModal] = useState(false); const [showRequestModal, setShowRequestModal] = useState(false);
const [showRequest4kModal, setShowRequest4kModal] = useState(false); const [showRequest4kModal, setShowRequest4kModal] = useState(false);
const [editRequest, setEditRequest] = useState(false);
const activeRequest = media?.requests.find( // All pending requests
(request) => request.status === MediaRequestStatus.PENDING && !request.is4k
);
const active4kRequest = media?.requests.find(
(request) => request.status === MediaRequestStatus.PENDING && request.is4k
);
// All pending
const activeRequests = media?.requests.filter( const activeRequests = media?.requests.filter(
(request) => request.status === MediaRequestStatus.PENDING && !request.is4k (request) => request.status === MediaRequestStatus.PENDING && !request.is4k
); );
const active4kRequests = media?.requests.filter( const active4kRequests = media?.requests.filter(
(request) => request.status === MediaRequestStatus.PENDING && request.is4k (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 ( const modifyRequest = async (
request: MediaRequest, request: MediaRequest,
type: 'approve' | 'decline' type: 'approve' | 'decline'
@ -115,57 +129,10 @@ const RequestButton: React.FC<RequestButtonProps> = ({
id: 'request', id: 'request',
text: intl.formatMessage(globalMessages.request), text: intl.formatMessage(globalMessages.request),
action: () => { action: () => {
setEditRequest(false);
setShowRequestModal(true); setShowRequestModal(true);
}, },
svg: ( svg: <DownloadIcon className="w-5 h-5 mr-1" />,
<svg
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</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
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
),
}); });
} }
@ -181,112 +148,44 @@ const RequestButton: React.FC<RequestButtonProps> = ({
id: 'request4k', id: 'request4k',
text: intl.formatMessage(globalMessages.request4k), text: intl.formatMessage(globalMessages.request4k),
action: () => { action: () => {
setEditRequest(false);
setShowRequest4kModal(true); setShowRequest4kModal(true);
}, },
svg: ( svg: <DownloadIcon className="w-5 h-5 mr-1" />,
<svg
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</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
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
),
}); });
} }
if ( if (
activeRequest && activeRequest &&
mediaType === 'movie' && (activeRequest.requestedBy.id === user?.id ||
hasPermission(Permission.REQUEST) (activeRequests?.length === 1 &&
hasPermission(Permission.MANAGE_REQUESTS)))
) { ) {
buttons.push({ buttons.push({
id: 'active-request', id: 'active-request',
text: intl.formatMessage(messages.viewrequest), text: intl.formatMessage(messages.viewrequest),
action: () => setShowRequestModal(true), action: () => {
svg: ( setEditRequest(true);
<svg setShowRequestModal(true);
className="w-4 mr-1" },
fill="currentColor" svg: <InformationCircleIcon className="w-5 h-5 mr-1" />,
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
),
}); });
} }
if ( if (
active4kRequest && active4kRequest &&
mediaType === 'movie' && (active4kRequest.requestedBy.id === user?.id ||
(hasPermission(Permission.REQUEST_4K) || (active4kRequests?.length === 1 &&
hasPermission(Permission.REQUEST_4K_MOVIE)) hasPermission(Permission.MANAGE_REQUESTS)))
) { ) {
buttons.push({ buttons.push({
id: 'active-4k-request', id: 'active-4k-request',
text: intl.formatMessage(messages.viewrequest4k), text: intl.formatMessage(messages.viewrequest4k),
action: () => setShowRequest4kModal(true), action: () => {
svg: ( setEditRequest(true);
<svg setShowRequest4kModal(true);
className="w-4 mr-1" },
fill="currentColor" svg: <InformationCircleIcon className="w-5 h-5 mr-1" />,
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
),
}); });
} }
@ -302,20 +201,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
action: () => { action: () => {
modifyRequest(activeRequest, 'approve'); modifyRequest(activeRequest, 'approve');
}, },
svg: ( svg: <CheckIcon className="w-5 h-5 mr-1" />,
<svg
className="w-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
),
}, },
{ {
id: 'decline-request', id: 'decline-request',
@ -323,20 +209,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
action: () => { action: () => {
modifyRequest(activeRequest, 'decline'); modifyRequest(activeRequest, 'decline');
}, },
svg: ( svg: <XIcon className="w-5 h-5 mr-1" />,
<svg
className="w-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
),
} }
); );
} }
@ -356,20 +229,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
action: () => { action: () => {
modifyRequests(activeRequests, 'approve'); modifyRequests(activeRequests, 'approve');
}, },
svg: ( svg: <CheckIcon className="w-5 h-5 mr-1" />,
<svg
className="w-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
),
}, },
{ {
id: 'decline-request-batch', id: 'decline-request-batch',
@ -379,20 +239,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
action: () => { action: () => {
modifyRequests(activeRequests, 'decline'); modifyRequests(activeRequests, 'decline');
}, },
svg: ( svg: <XIcon className="w-5 h-5 mr-1" />,
<svg
className="w-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
),
} }
); );
} }
@ -409,20 +256,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
action: () => { action: () => {
modifyRequest(active4kRequest, 'approve'); modifyRequest(active4kRequest, 'approve');
}, },
svg: ( svg: <CheckIcon className="w-5 h-5 mr-1" />,
<svg
className="w-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
),
}, },
{ {
id: 'decline-4k-request', id: 'decline-4k-request',
@ -430,20 +264,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
action: () => { action: () => {
modifyRequest(active4kRequest, 'decline'); modifyRequest(active4kRequest, 'decline');
}, },
svg: ( svg: <XIcon className="w-5 h-5 mr-1" />,
<svg
className="w-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
),
} }
); );
} }
@ -456,54 +277,71 @@ const RequestButton: React.FC<RequestButtonProps> = ({
) { ) {
buttons.push( buttons.push(
{ {
id: 'approve-request-batch', id: 'approve-4k-request-batch',
text: intl.formatMessage(messages.approve4krequests, { text: intl.formatMessage(messages.approve4krequests, {
requestCount: active4kRequests.length, requestCount: active4kRequests.length,
}), }),
action: () => { action: () => {
modifyRequests(active4kRequests, 'approve'); modifyRequests(active4kRequests, 'approve');
}, },
svg: ( svg: <CheckIcon className="w-5 h-5 mr-1" />,
<svg
className="w-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
),
}, },
{ {
id: 'decline-request-batch', id: 'decline-4k-request-batch',
text: intl.formatMessage(messages.decline4krequests, { text: intl.formatMessage(messages.decline4krequests, {
requestCount: active4kRequests.length, requestCount: active4kRequests.length,
}), }),
action: () => { action: () => {
modifyRequests(active4kRequests, 'decline'); modifyRequests(active4kRequests, 'decline');
}, },
svg: ( svg: <XIcon className="w-5 h-5 mr-1" />,
<svg
className="w-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</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: <DownloadIcon className="w-5 h-5 mr-1" />,
});
}
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: <DownloadIcon className="w-5 h-5 mr-1" />,
});
}
const [buttonOne, ...others] = buttons; const [buttonOne, ...others] = buttons;
if (!buttonOne) { if (!buttonOne) {
@ -516,6 +354,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
tmdbId={tmdbId} tmdbId={tmdbId}
show={showRequestModal} show={showRequestModal}
type={mediaType} type={mediaType}
editRequest={editRequest ? activeRequest : undefined}
onComplete={() => { onComplete={() => {
onUpdate(); onUpdate();
setShowRequestModal(false); setShowRequestModal(false);
@ -526,6 +365,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
tmdbId={tmdbId} tmdbId={tmdbId}
show={showRequest4kModal} show={showRequest4kModal}
type={mediaType} type={mediaType}
editRequest={editRequest ? active4kRequest : undefined}
is4k is4k
onComplete={() => { onComplete={() => {
onUpdate(); onUpdate();

@ -1,3 +1,4 @@
import { CheckIcon, TrashIcon, XIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import Link from 'next/link'; import Link from 'next/link';
import React, { useContext, useEffect } from 'react'; import React, { useContext, useEffect } from 'react';
@ -68,20 +69,7 @@ const RequestCardError: React.FC<RequestCardErrorProps> = ({ mediaId }) => {
buttonSize="sm" buttonSize="sm"
onClick={() => deleteRequest()} onClick={() => deleteRequest()}
> >
<svg <TrashIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
<span>{intl.formatMessage(messages.deleterequest)}</span> <span>{intl.formatMessage(messages.deleterequest)}</span>
</Button> </Button>
</div> </div>
@ -261,18 +249,7 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
buttonSize="sm" buttonSize="sm"
onClick={() => modifyRequest('approve')} onClick={() => modifyRequest('approve')}
> >
<svg <CheckIcon className="w-4 h-4 mr-0 sm:mr-1" />
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block"> <span className="hidden sm:block">
{intl.formatMessage(globalMessages.approve)} {intl.formatMessage(globalMessages.approve)}
</span> </span>
@ -284,18 +261,7 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
buttonSize="sm" buttonSize="sm"
onClick={() => modifyRequest('decline')} onClick={() => modifyRequest('decline')}
> >
<svg <XIcon className="w-4 h-4 mr-0 sm:mr-1" />
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block"> <span className="hidden sm:block">
{intl.formatMessage(globalMessages.decline)} {intl.formatMessage(globalMessages.decline)}
</span> </span>

@ -1,3 +1,10 @@
import {
CheckIcon,
PencilIcon,
RefreshIcon,
TrashIcon,
XIcon,
} from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import Link from 'next/link'; import Link from 'next/link';
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
@ -29,6 +36,7 @@ const messages = defineMessages({
modified: 'Modified', modified: 'Modified',
modifieduserdate: '{date} by {user}', modifieduserdate: '{date} by {user}',
mediaerror: 'The associated title for this request is no longer available.', mediaerror: 'The associated title for this request is no longer available.',
editrequest: 'Edit Request',
deleterequest: 'Delete Request', deleterequest: 'Delete Request',
cancelRequest: 'Cancel Request', cancelRequest: 'Cancel Request',
}); });
@ -66,20 +74,7 @@ const RequestItemError: React.FC<RequestItemErroProps> = ({
buttonSize="sm" buttonSize="sm"
onClick={() => deleteRequest()} onClick={() => deleteRequest()}
> >
<svg <TrashIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
<span>{intl.formatMessage(messages.deleterequest)}</span> <span>{intl.formatMessage(messages.deleterequest)}</span>
</Button> </Button>
</div> </div>
@ -369,31 +364,6 @@ const RequestItem: React.FC<RequestItemProps> = ({
</div> </div>
</div> </div>
<div className="z-10 flex flex-col justify-center w-full pl-4 pr-4 mt-4 space-y-2 xl:mt-0 xl:items-end xl:w-96 xl:pl-0"> <div className="z-10 flex flex-col justify-center w-full pl-4 pr-4 mt-4 space-y-2 xl:mt-0 xl:items-end xl:w-96 xl:pl-0">
{requestData.status === MediaRequestStatus.PENDING &&
!hasPermission(Permission.MANAGE_REQUESTS) &&
requestData.requestedBy.id === user?.id && (
<ConfirmButton
onClick={() => deleteRequest()}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<svg
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
<span className="block">
{intl.formatMessage(messages.cancelRequest)}
</span>
</ConfirmButton>
)}
{requestData.media[requestData.is4k ? 'status4k' : 'status'] === {requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.UNKNOWN && MediaStatus.UNKNOWN &&
requestData.status !== MediaRequestStatus.DECLINED && requestData.status !== MediaRequestStatus.DECLINED &&
@ -404,19 +374,14 @@ const RequestItem: React.FC<RequestItemProps> = ({
disabled={isRetrying} disabled={isRetrying}
onClick={() => retryRequest()} onClick={() => retryRequest()}
> >
<svg <RefreshIcon
className="w-5 h-5 mr-1" className={`w-5 h-5 mr-1 ${isRetrying ? 'animate-spin' : ''}`}
fill="currentColor" style={{ animationDirection: 'reverse' }}
xmlns="http://www.w3.org/2000/svg" />
viewBox="0 0 24 24"
width="18px"
height="18px"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z" />
</svg>
<span className="block"> <span className="block">
{intl.formatMessage(globalMessages.retry)} {intl.formatMessage(
isRetrying ? globalMessages.retrying : globalMessages.retry
)}
</span> </span>
</Button> </Button>
)} )}
@ -427,94 +392,72 @@ const RequestItem: React.FC<RequestItemProps> = ({
confirmText={intl.formatMessage(globalMessages.areyousure)} confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full" className="w-full"
> >
<svg <TrashIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
<span className="block"> <span className="block">
{intl.formatMessage(globalMessages.delete)} {intl.formatMessage(messages.deleterequest)}
</span> </span>
</ConfirmButton> </ConfirmButton>
)} )}
{requestData.status === MediaRequestStatus.PENDING && {requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && ( hasPermission(Permission.MANAGE_REQUESTS) && (
<> <div className="flex flex-row w-full space-x-2">
<div className="flex flex-row w-full space-x-2"> <span className="w-full">
<span className="w-full"> <Button
<Button className="w-full"
className="w-full" buttonType="success"
buttonType="success" onClick={() => modifyRequest('approve')}
onClick={() => modifyRequest('approve')} >
> <CheckIcon className="w-5 h-5 mr-1" />
<svg <span className="block">
className="w-5 h-5 mr-1" {intl.formatMessage(globalMessages.approve)}
fill="currentColor" </span>
viewBox="0 0 20 20" </Button>
xmlns="http://www.w3.org/2000/svg" </span>
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="block">
{intl.formatMessage(globalMessages.approve)}
</span>
</Button>
</span>
<span className="w-full">
<Button
className="w-full"
buttonType="danger"
onClick={() => modifyRequest('decline')}
>
<svg
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
<span className="block">
{intl.formatMessage(globalMessages.decline)}
</span>
</Button>
</span>
</div>
<span className="w-full"> <span className="w-full">
<Button <Button
className="w-full" className="w-full"
buttonType="primary" buttonType="danger"
onClick={() => setShowEditModal(true)} onClick={() => modifyRequest('decline')}
> >
<svg <XIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
<span className="block"> <span className="block">
{intl.formatMessage(globalMessages.edit)} {intl.formatMessage(globalMessages.decline)}
</span> </span>
</Button> </Button>
</span> </span>
</> </div>
)}
{requestData.status === MediaRequestStatus.PENDING &&
(hasPermission(Permission.MANAGE_REQUESTS) ||
(requestData.requestedBy.id === user?.id &&
(requestData.type === 'tv' ||
hasPermission(Permission.REQUEST_ADVANCED)))) && (
<span className="w-full">
<Button
className="w-full"
buttonType="primary"
onClick={() => setShowEditModal(true)}
>
<PencilIcon className="w-5 h-5 mr-1" />
<span className="block">
{intl.formatMessage(messages.editrequest)}
</span>
</Button>
</span>
)}
{requestData.status === MediaRequestStatus.PENDING &&
!hasPermission(Permission.MANAGE_REQUESTS) &&
requestData.requestedBy.id === user?.id && (
<ConfirmButton
onClick={() => deleteRequest()}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<XIcon className="w-5 h-5 mr-1" />
<span className="block">
{intl.formatMessage(messages.cancelRequest)}
</span>
</ConfirmButton>
)} )}
</div> </div>
</div> </div>

@ -1,3 +1,4 @@
import { FilterIcon, SortDescendingIcon } from '@heroicons/react/solid';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@ -119,18 +120,7 @@ const RequestList: React.FC = () => {
<div className="flex flex-col flex-grow mt-2 sm:flex-row lg:flex-grow-0"> <div className="flex flex-col flex-grow mt-2 sm:flex-row lg:flex-grow-0">
<div className="flex flex-grow mb-2 sm:mb-0 sm:mr-2 lg:flex-grow-0"> <div className="flex flex-grow mb-2 sm:mb-0 sm:mr-2 lg:flex-grow-0">
<span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md"> <span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md">
<svg <FilterIcon className="w-6 h-6" />
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z"
clipRule="evenodd"
/>
</svg>
</span> </span>
<select <select
id="filter" id="filter"
@ -169,14 +159,7 @@ const RequestList: React.FC = () => {
</div> </div>
<div className="flex flex-grow mb-2 sm:mb-0 lg:flex-grow-0"> <div className="flex flex-grow mb-2 sm:mb-0 lg:flex-grow-0">
<span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default sm:text-sm rounded-l-md"> <span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default sm:text-sm rounded-l-md">
<svg <SortDescendingIcon className="w-6 h-6" />
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3 3a1 1 0 000 2h11a1 1 0 100-2H3zM3 7a1 1 0 000 2h7a1 1 0 100-2H3zM3 11a1 1 0 100 2h4a1 1 0 100-2H3zM15 8a1 1 0 10-2 0v5.586l-1.293-1.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L15 13.586V8z" />
</svg>
</span> </span>
<select <select
id="sort" id="sort"

@ -1,5 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { Listbox, Transition } from '@headlessui/react'; import { Listbox, Transition } from '@headlessui/react';
import {
AdjustmentsIcon,
CheckIcon,
ChevronDownIcon,
} from '@heroicons/react/solid';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@ -269,15 +274,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
return ( return (
<> <>
<div className="flex items-center mb-2 font-bold tracking-wider"> <div className="flex items-center mb-2 font-bold tracking-wider">
<svg <AdjustmentsIcon className="w-5 h-5 mr-1" />
className="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M9.707 7.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L13 8.586V5h3a2 2 0 012 2v5a2 2 0 01-2 2H8a2 2 0 01-2-2V7a2 2 0 012-2h3v3.586L9.707 7.293zM11 3a1 1 0 112 0v2h-2V3z" />
<path d="M4 9a2 2 0 00-2 2v5a2 2 0 002 2h8a2 2 0 002-2H4V9z" />
</svg>
{intl.formatMessage(messages.advancedoptions)} {intl.formatMessage(messages.advancedoptions)}
</div> </div>
<div className="p-4 bg-gray-600 rounded-md shadow"> <div className="p-4 bg-gray-600 rounded-md shadow">
@ -526,19 +523,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
</span> </span>
</span> </span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> <span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<svg <ChevronDownIcon className="w-5 h-5 text-gray-500" />
className="w-5 h-5 text-gray-500"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span> </span>
</Listbox.Button> </Listbox.Button>
</span> </span>
@ -592,18 +577,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
: 'text-indigo-600' : 'text-indigo-600'
} absolute inset-y-0 left-0 flex items-center pl-1.5`} } absolute inset-y-0 left-0 flex items-center pl-1.5`}
> >
<svg <CheckIcon className="w-5 h-5" />
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span> </span>
)} )}
</div> </div>

@ -1,17 +1,14 @@
import { DownloadIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
import { import { MediaStatus } from '../../../server/constants/media';
MediaRequestStatus,
MediaStatus,
} from '../../../server/constants/media';
import { MediaRequest } from '../../../server/entity/MediaRequest'; import { MediaRequest } from '../../../server/entity/MediaRequest';
import { QuotaResponse } from '../../../server/interfaces/api/userInterfaces'; import { QuotaResponse } from '../../../server/interfaces/api/userInterfaces';
import { Permission } from '../../../server/lib/permissions'; import { Permission } from '../../../server/lib/permissions';
import { MovieDetails } from '../../../server/models/Movie'; import { MovieDetails } from '../../../server/models/Movie';
import DownloadIcon from '../../assets/download.svg';
import { useUser } from '../../hooks/useUser'; import { useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
import Alert from '../Common/Alert'; import Alert from '../Common/Alert';
@ -25,11 +22,11 @@ const messages = defineMessages({
requestCancel: 'Request for <strong>{title}</strong> canceled.', requestCancel: 'Request for <strong>{title}</strong> canceled.',
requesttitle: 'Request {title}', requesttitle: 'Request {title}',
request4ktitle: 'Request {title} in 4K', request4ktitle: 'Request {title} in 4K',
edit: 'Edit Request',
cancel: 'Cancel Request', cancel: 'Cancel Request',
pendingrequest: 'Pending Request for {title}', pendingrequest: 'Pending Request for {title}',
pending4krequest: 'Pending Request for {title} in 4K', pending4krequest: 'Pending 4K Request for {title}',
requestfrom: 'There is currently a pending request from {username}.', requestfrom: "{username}'s request is pending approval.",
request4kfrom: 'There is currently a pending 4K request from {username}.',
errorediting: 'Something went wrong while editing the request.', errorediting: 'Something went wrong while editing the request.',
requestedited: 'Request for <strong>{title}</strong> edited successfully!', requestedited: 'Request for <strong>{title}</strong> edited successfully!',
requesterror: 'Something went wrong while submitting the request.', requesterror: 'Something went wrong while submitting the request.',
@ -130,18 +127,14 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
} finally { } finally {
setIsUpdating(false); setIsUpdating(false);
} }
}, [data, onComplete, addToast, requestOverrides]); }, [data, onComplete, addToast, requestOverrides, hasPermission, intl, is4k]);
const activeRequest = data?.mediaInfo?.requests?.find(
(request) => request.is4k === !!is4k
);
const cancelRequest = async () => { const cancelRequest = async () => {
setIsUpdating(true); setIsUpdating(true);
try { try {
const response = await axios.delete<MediaRequest>( const response = await axios.delete<MediaRequest>(
`/api/v1/request/${activeRequest?.id}` `/api/v1/request/${editRequest?.id}`
); );
if (response.status === 204) { if (response.status === 204) {
@ -206,11 +199,15 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
} }
}; };
const isOwner = activeRequest if (editRequest) {
? activeRequest.requestedBy.id === user?.id const isOwner = editRequest.requestedBy.id === user?.id;
: false; const showEditButton = hasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_ADVANCED],
{
type: 'or',
}
);
if (activeRequest?.status === MediaRequestStatus.PENDING) {
return ( return (
<Modal <Modal
loading={!data && !error} loading={!data && !error}
@ -218,48 +215,47 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
onCancel={onCancel} onCancel={onCancel}
title={intl.formatMessage( title={intl.formatMessage(
is4k ? messages.pending4krequest : messages.pendingrequest, is4k ? messages.pending4krequest : messages.pendingrequest,
{ { title: data?.title }
title: data?.title,
}
)} )}
onOk={() => (isOwner ? cancelRequest() : updateRequest())} onOk={() => (showEditButton ? updateRequest() : cancelRequest())}
okDisabled={isUpdating} okDisabled={isUpdating}
okText={ okText={
isOwner showEditButton
? isUpdating ? intl.formatMessage(messages.edit)
? intl.formatMessage(globalMessages.canceling) : intl.formatMessage(messages.cancel)
: intl.formatMessage(messages.cancel)
: intl.formatMessage(globalMessages.edit)
} }
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)} cancelText={intl.formatMessage(globalMessages.close)}
iconSvg={<DownloadIcon className="w-6 h-6" />} iconSvg={<DownloadIcon className="w-6 h-6" />}
> >
{isOwner {isOwner
? intl.formatMessage(messages.pendingapproval) ? intl.formatMessage(messages.pendingapproval)
: intl.formatMessage( : intl.formatMessage(messages.requestfrom, {
is4k ? messages.request4kfrom : messages.requestfrom, username: editRequest.requestedBy.displayName,
{ })}
username: activeRequest.requestedBy.displayName,
}
)}
{(hasPermission(Permission.REQUEST_ADVANCED) || {(hasPermission(Permission.REQUEST_ADVANCED) ||
hasPermission(Permission.MANAGE_REQUESTS)) && ( hasPermission(Permission.MANAGE_REQUESTS)) && (
<div className="mt-4"> <div className="mt-4">
<AdvancedRequester <AdvancedRequester
type="movie" type="movie"
is4k={is4k} is4k={is4k}
requestUser={editRequest?.requestedBy} requestUser={editRequest.requestedBy}
defaultOverrides={ defaultOverrides={{
editRequest folder: editRequest.rootFolder,
? { profile: editRequest.profileId,
folder: editRequest.rootFolder, server: editRequest.serverId,
profile: editRequest.profileId, tags: editRequest.tags,
server: editRequest.serverId, }}
tags: editRequest.tags,
}
: undefined
}
onChange={(overrides) => { onChange={(overrides) => {
setRequestOverrides(overrides); setRequestOverrides(overrides);
}} }}

@ -1,3 +1,4 @@
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid';
import Link from 'next/link'; import Link from 'next/link';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@ -90,31 +91,9 @@ const QuotaDisplay: React.FC<QuotaDisplayProps> = ({
</div> </div>
<div className="flex justify-end flex-1"> <div className="flex justify-end flex-1">
{showDetails ? ( {showDetails ? (
<svg <ChevronUpIcon className="w-6 h-6" />
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z"
clipRule="evenodd"
/>
</svg>
) : ( ) : (
<svg <ChevronDownIcon className="w-6 h-6" />
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
)} )}
</div> </div>
</div> </div>

@ -1,3 +1,4 @@
import { DownloadIcon } from '@heroicons/react/outline';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
@ -51,22 +52,7 @@ const SearchByNameModal: React.FC<SearchByNameModalProps> = ({
okText={intl.formatMessage(globalMessages.next)} okText={intl.formatMessage(globalMessages.next)}
okDisabled={!tvdbId} okDisabled={!tvdbId}
okButtonType="primary" okButtonType="primary"
iconSvg={ iconSvg={<DownloadIcon className="w-6 h-6" />}
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
}
> >
<Alert <Alert
title={intl.formatMessage(messages.notvdbiddescription)} title={intl.formatMessage(messages.notvdbiddescription)}

@ -1,3 +1,4 @@
import { DownloadIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@ -28,6 +29,11 @@ const messages = defineMessages({
requestSuccess: '<strong>{title}</strong> requested successfully!', requestSuccess: '<strong>{title}</strong> requested successfully!',
requesttitle: 'Request {title}', requesttitle: 'Request {title}',
request4ktitle: 'Request {title} in 4K', 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: requestseasons:
'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}', 'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}',
requestall: 'Request All Seasons', requestall: 'Request All Seasons',
@ -42,6 +48,7 @@ const messages = defineMessages({
requestcancelled: 'Request for <strong>{title}</strong> canceled.', requestcancelled: 'Request for <strong>{title}</strong> canceled.',
autoapproval: 'Automatic Approval', autoapproval: 'Automatic Approval',
requesterror: 'Something went wrong while submitting the request.', requesterror: 'Something went wrong while submitting the request.',
pendingapproval: 'Your request is pending approval.',
}); });
interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> { interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
@ -341,6 +348,8 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
return seasonRequest; return seasonRequest;
}; };
const isOwner = editRequest && editRequest.requestedBy.id === user?.id;
return !data?.externalIds.tvdbId && searchModal.show ? ( return !data?.externalIds.tvdbId && searchModal.show ? (
<SearchByNameModal <SearchByNameModal
tvdbId={tvdbId} tvdbId={tvdbId}
@ -361,12 +370,20 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
onCancel={tvdbId ? () => setSearchModal({ show: true }) : onCancel} onCancel={tvdbId ? () => setSearchModal({ show: true }) : onCancel}
onOk={() => (editRequest ? updateRequest() : sendRequest())} onOk={() => (editRequest ? updateRequest() : sendRequest())}
title={intl.formatMessage( title={intl.formatMessage(
is4k ? messages.request4ktitle : messages.requesttitle, editRequest
? is4k
? messages.pending4krequest
: messages.pendingrequest
: is4k
? messages.request4ktitle
: messages.requesttitle,
{ title: data?.name } { title: data?.name }
)} )}
okText={ okText={
editRequest && selectedSeasons.length === 0 editRequest
? 'Cancel Request' ? selectedSeasons.length === 0
? intl.formatMessage(messages.cancel)
: intl.formatMessage(messages.edit)
: getAllRequestedSeasons().length >= getAllSeasons().length : getAllRequestedSeasons().length >= getAllSeasons().length
? intl.formatMessage(messages.alreadyrequested) ? intl.formatMessage(messages.alreadyrequested)
: !settings.currentSettings.partialRequestsEnabled : !settings.currentSettings.partialRequestsEnabled
@ -396,27 +413,21 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
: `primary` : `primary`
} }
cancelText={ cancelText={
tvdbId editRequest
? intl.formatMessage(globalMessages.close)
: tvdbId
? intl.formatMessage(globalMessages.back) ? intl.formatMessage(globalMessages.back)
: intl.formatMessage(globalMessages.cancel) : intl.formatMessage(globalMessages.cancel)
} }
iconSvg={ iconSvg={<DownloadIcon className="w-6 h-6" />}
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
}
> >
{editRequest
? isOwner
? intl.formatMessage(messages.pendingapproval)
: intl.formatMessage(messages.requestfrom, {
username: editRequest?.requestedBy.displayName,
})
: null}
{hasPermission( {hasPermission(
[ [
Permission.MANAGE_REQUESTS, Permission.MANAGE_REQUESTS,

@ -1,3 +1,4 @@
import { AtSymbolIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import Link from 'next/link'; import Link from 'next/link';
@ -123,6 +124,7 @@ const ResetPassword: React.FC = () => {
type="submit" type="submit"
disabled={isSubmitting || !isValid} disabled={isSubmitting || !isValid}
> >
<AtSymbolIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.emailresetlink)} {intl.formatMessage(messages.emailresetlink)}
</Button> </Button>
</span> </span>

@ -1,7 +1,8 @@
import { ClipboardCopyIcon } from '@heroicons/react/solid';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import useClipboard from 'react-use-clipboard';
import { useToasts } from 'react-toast-notifications';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useClipboard from 'react-use-clipboard';
const messages = defineMessages({ const messages = defineMessages({
copied: 'Copied API key to clipboard.', copied: 'Copied API key to clipboard.',
@ -29,17 +30,9 @@ const CopyButton: React.FC<{ textToCopy: string }> = ({ textToCopy }) => {
e.preventDefault(); e.preventDefault();
setCopied(); 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"
> >
<svg <ClipboardCopyIcon className="w-5 h-5 text-white" />
className="w-5 h-5 text-white"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M8 2a1 1 0 000 2h2a1 1 0 100-2H8z" />
<path d="M3 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v6h-4.586l1.293-1.293a1 1 0 00-1.414-1.414l-3 3a1 1 0 000 1.414l3 3a1 1 0 001.414-1.414L10.414 13H15v3a2 2 0 01-2 2H5a2 2 0 01-2-2V5zM15 11h2a1 1 0 110 2h-2v-2z" />
</svg>
</button> </button>
); );
}; };

@ -1,3 +1,4 @@
import { CheckIcon, XIcon } from '@heroicons/react/solid';
import React from 'react'; import React from 'react';
interface LibraryItemProps { interface LibraryItemProps {
@ -12,8 +13,8 @@ const LibraryItem: React.FC<LibraryItemProps> = ({
onToggle, onToggle,
}) => { }) => {
return ( return (
<li className="col-span-1 flex shadow-sm rounded-md"> <li className="flex col-span-1 rounded-md shadow-sm">
<div className="flex-1 flex items-center justify-between border-t border-r border-b border-gray-700 bg-gray-600 rounded-md truncate"> <div className="flex items-center justify-between flex-1 truncate bg-gray-600 border-t border-b border-r border-gray-700 rounded-md">
<div className="flex-1 px-4 py-6 text-sm leading-5 truncate cursor-default"> <div className="flex-1 px-4 py-6 text-sm leading-5 truncate cursor-default">
{name} {name}
</div> </div>
@ -45,19 +46,7 @@ const LibraryItem: React.FC<LibraryItemProps> = ({
: 'opacity-100 ease-in duration-200' : 'opacity-100 ease-in duration-200'
} absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`} } absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`}
> >
<svg <XIcon className="w-3 h-3 text-gray-400" />
className="h-3 w-3 text-gray-400"
fill="none"
viewBox="0 0 12 12"
>
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span> </span>
<span <span
className={`${ className={`${
@ -66,13 +55,7 @@ const LibraryItem: React.FC<LibraryItemProps> = ({
: 'opacity-0 ease-out duration-100' : 'opacity-0 ease-out duration-100'
} absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`} } absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`}
> >
<svg <CheckIcon className="w-3 h-3 text-indigo-600" />
className="h-3 w-3 text-indigo-600"
fill="currentColor"
viewBox="0 0 12 12"
>
<path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" />
</svg>
</span> </span>
</span> </span>
</span> </span>

@ -18,7 +18,7 @@ const messages = defineMessages({
webhookUrlPlaceholder: 'Server Settings → Integrations → Webhooks', webhookUrlPlaceholder: 'Server Settings → Integrations → Webhooks',
discordsettingssaved: 'Discord notification settings saved successfully!', discordsettingssaved: 'Discord notification settings saved successfully!',
discordsettingsfailed: 'Discord notification settings failed to save.', discordsettingsfailed: 'Discord notification settings failed to save.',
testsent: 'Discord test notification sent!', discordtestsent: 'Discord test notification sent!',
validationUrl: 'You must provide a valid URL', 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', appearance: 'info',
autoDismiss: true, autoDismiss: true,
}); });

@ -24,7 +24,7 @@ const messages = defineMessages({
authPass: 'SMTP Password', authPass: 'SMTP Password',
emailsettingssaved: 'Email notification settings saved successfully!', emailsettingssaved: 'Email notification settings saved successfully!',
emailsettingsfailed: 'Email notification settings failed to save.', emailsettingsfailed: 'Email notification settings failed to save.',
testsent: 'Email test notification sent!', emailtestsent: 'Email test notification sent!',
allowselfsigned: 'Allow Self-Signed Certificates', allowselfsigned: 'Allow Self-Signed Certificates',
ssldisabletip: ssldisabletip:
'SSL should be disabled on standard TLS connections (port 587)', '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', appearance: 'info',
autoDismiss: true, autoDismiss: true,
}); });

@ -22,7 +22,7 @@ const messages = defineMessages({
validationChatIdRequired: 'You must provide a valid chat ID', validationChatIdRequired: 'You must provide a valid chat ID',
telegramsettingssaved: 'Telegram notification settings saved successfully!', telegramsettingssaved: 'Telegram notification settings saved successfully!',
telegramsettingsfailed: 'Telegram notification settings failed to save.', telegramsettingsfailed: 'Telegram notification settings failed to save.',
testsent: 'Telegram test notification sent!', telegramtestsent: 'Telegram test notification sent!',
settinguptelegramDescription: settinguptelegramDescription:
'To configure Telegram notifications, you will need to <CreateBotLink>create a bot</CreateBotLink> 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 <GetIdBotLink>@get_id_bot</GetIdBotLink> to the chat and issuing the <code>/my_id</code> command.', 'To configure Telegram notifications, you will need to <CreateBotLink>create a bot</CreateBotLink> 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 <GetIdBotLink>@get_id_bot</GetIdBotLink> to the chat and issuing the <code>/my_id</code> command.',
sendSilently: 'Send Silently', sendSilently: 'Send Silently',
@ -113,7 +113,7 @@ const NotificationsTelegram: React.FC = () => {
}, },
}); });
addToast(intl.formatMessage(messages.testsent), { addToast(intl.formatMessage(messages.telegramtestsent), {
appearance: 'info', appearance: 'info',
autoDismiss: true, autoDismiss: true,
}); });

@ -1,3 +1,4 @@
import { QuestionMarkCircleIcon, RefreshIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
@ -232,18 +233,7 @@ const NotificationsWebhook: React.FC = () => {
}} }}
className="mr-2" className="mr-2"
> >
<svg <RefreshIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
clipRule="evenodd"
/>
</svg>
{intl.formatMessage(messages.resetPayload)} {intl.formatMessage(messages.resetPayload)}
</Button> </Button>
<a <a
@ -252,18 +242,7 @@ const NotificationsWebhook: React.FC = () => {
rel="noreferrer" 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" 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"
> >
<svg <QuestionMarkCircleIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z"
clipRule="evenodd"
/>
</svg>
{intl.formatMessage(messages.templatevariablehelp)} {intl.formatMessage(messages.templatevariablehelp)}
</a> </a>
</div> </div>

@ -1,3 +1,4 @@
import { PencilIcon, PlusIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import { Field, Formik } from 'formik'; import { Field, Formik } from 'formik';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
@ -62,7 +63,7 @@ const messages = defineMessages({
loadingTags: 'Loading tags…', loadingTags: 'Loading tags…',
testFirstTags: 'Test connection to load tags', testFirstTags: 'Test connection to load tags',
tags: 'Tags', tags: 'Tags',
preventSearch: 'Disable Auto-Search', enableSearch: 'Enable Automatic Search',
validationApplicationUrl: 'You must provide a valid URL', validationApplicationUrl: 'You must provide a valid URL',
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
validationBaseUrlLeadingSlash: 'Base URL must have a leading slash', validationBaseUrlLeadingSlash: 'Base URL must have a leading slash',
@ -256,8 +257,8 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
isDefault: radarr?.isDefault ?? false, isDefault: radarr?.isDefault ?? false,
is4k: radarr?.is4k ?? false, is4k: radarr?.is4k ?? false,
externalUrl: radarr?.externalUrl, externalUrl: radarr?.externalUrl,
syncEnabled: radarr?.syncEnabled, syncEnabled: radarr?.syncEnabled ?? false,
preventSearch: radarr?.preventSearch, enableSearch: !radarr?.preventSearch,
}} }}
validationSchema={RadarrSettingsSchema} validationSchema={RadarrSettingsSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
@ -282,7 +283,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
isDefault: values.isDefault, isDefault: values.isDefault,
externalUrl: values.externalUrl, externalUrl: values.externalUrl,
syncEnabled: values.syncEnabled, syncEnabled: values.syncEnabled,
preventSearch: values.preventSearch, preventSearch: !values.enableSearch,
}; };
if (!radarr) { if (!radarr) {
await axios.post('/api/v1/settings/radarr', submission); await axios.post('/api/v1/settings/radarr', submission);
@ -356,6 +357,13 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
values.is4k ? messages.edit4kradarr : messages.editradarr values.is4k ? messages.edit4kradarr : messages.editradarr
) )
} }
iconSvg={
!radarr ? (
<PlusIcon className="w-6 h-6" />
) : (
<PencilIcon className="w-6 h-6" />
)
}
> >
<div className="mb-6"> <div className="mb-6">
<div className="form-row"> <div className="form-row">
@ -701,14 +709,14 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
</div> </div>
</div> </div>
<div className="form-row"> <div className="form-row">
<label htmlFor="preventSearch" className="checkbox-label"> <label htmlFor="enableSearch" className="checkbox-label">
{intl.formatMessage(messages.preventSearch)} {intl.formatMessage(messages.enableSearch)}
</label> </label>
<div className="form-input"> <div className="form-input">
<Field <Field
type="checkbox" type="checkbox"
id="preventSearch" id="enableSearch"
name="preventSearch" name="enableSearch"
/> />
</div> </div>
</div> </div>

@ -1,3 +1,4 @@
import { DocumentTextIcon } from '@heroicons/react/outline';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
@ -70,22 +71,7 @@ const Release: React.FC<ReleaseProps> = ({
> >
<Modal <Modal
onCancel={() => setModalOpen(false)} onCancel={() => setModalOpen(false)}
iconSvg={ iconSvg={<DocumentTextIcon className="w-6 h-6" />}
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
}
title={intl.formatMessage(messages.versionChangelog)} title={intl.formatMessage(messages.versionChangelog)}
cancelText={intl.formatMessage(globalMessages.close)} cancelText={intl.formatMessage(globalMessages.close)}
okText={intl.formatMessage(messages.viewongithub)} okText={intl.formatMessage(messages.viewongithub)}
@ -126,6 +112,7 @@ const Release: React.FC<ReleaseProps> = ({
</div> </div>
<div className="flex-1 text-center sm:text-right"> <div className="flex-1 text-center sm:text-right">
<Button buttonType="primary" onClick={() => setModalOpen(true)}> <Button buttonType="primary" onClick={() => setModalOpen(true)}>
<DocumentTextIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.viewchangelog)} {intl.formatMessage(messages.viewchangelog)}
</Button> </Button>
</div> </div>

@ -1,3 +1,4 @@
import { PlayIcon, StopIcon, TrashIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import React from 'react'; import React from 'react';
import { import {
@ -146,12 +147,12 @@ const SettingsJobs: React.FC = () => {
<tr key={`job-list-${job.id}`}> <tr key={`job-list-${job.id}`}>
<Table.TD> <Table.TD>
<div className="flex items-center text-sm leading-5 text-white"> <div className="flex items-center text-sm leading-5 text-white">
{job.running && <Spinner className="w-5 h-5 mr-2" />}
<span> <span>
{intl.formatMessage( {intl.formatMessage(
messages[job.id] ?? messages.unknownJob messages[job.id] ?? messages.unknownJob
)} )}
</span> </span>
{job.running && <Spinner className="w-5 h-5 ml-2" />}
</div> </div>
</Table.TD> </Table.TD>
<Table.TD> <Table.TD>
@ -180,10 +181,12 @@ const SettingsJobs: React.FC = () => {
<Table.TD alignText="right"> <Table.TD alignText="right">
{job.running ? ( {job.running ? (
<Button buttonType="danger" onClick={() => cancelJob(job)}> <Button buttonType="danger" onClick={() => cancelJob(job)}>
<StopIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.canceljob)} {intl.formatMessage(messages.canceljob)}
</Button> </Button>
) : ( ) : (
<Button buttonType="primary" onClick={() => runJob(job)}> <Button buttonType="primary" onClick={() => runJob(job)}>
<PlayIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.runnow)} {intl.formatMessage(messages.runnow)}
</Button> </Button>
)} )}
@ -223,6 +226,7 @@ const SettingsJobs: React.FC = () => {
<Table.TD>{formatBytes(cache.stats.vsize)}</Table.TD> <Table.TD>{formatBytes(cache.stats.vsize)}</Table.TD>
<Table.TD alignText="right"> <Table.TD alignText="right">
<Button buttonType="danger" onClick={() => flushCache(cache)}> <Button buttonType="danger" onClick={() => flushCache(cache)}>
<TrashIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.flushcache)} {intl.formatMessage(messages.flushcache)}
</Button> </Button>
</Table.TD> </Table.TD>

@ -1,3 +1,10 @@
import {
ClipboardCopyIcon,
DocumentSearchIcon,
FilterIcon,
PauseIcon,
PlayIcon,
} from '@heroicons/react/solid';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@ -135,6 +142,7 @@ const SettingsLogs: React.FC = () => {
> >
<Modal <Modal
title={intl.formatMessage(messages.logDetails)} title={intl.formatMessage(messages.logDetails)}
iconSvg={<DocumentSearchIcon className="w-6 h-6" />}
onCancel={() => setActiveLog(null)} onCancel={() => setActiveLog(null)}
cancelText={intl.formatMessage(globalMessages.close)} cancelText={intl.formatMessage(globalMessages.close)}
onOk={() => (activeLog ? copyLogString(activeLog) : undefined)} onOk={() => (activeLog ? copyLogString(activeLog) : undefined)}
@ -237,31 +245,9 @@ const SettingsLogs: React.FC = () => {
> >
<span> <span>
{refreshInterval ? ( {refreshInterval ? (
<svg <PauseIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
) : ( ) : (
<svg <PlayIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
clipRule="evenodd"
/>
</svg>
)} )}
</span> </span>
<span> <span>
@ -273,18 +259,7 @@ const SettingsLogs: React.FC = () => {
</div> </div>
<div className="flex flex-1 mb-2 sm:mb-0 sm:flex-none"> <div className="flex flex-1 mb-2 sm:mb-0 sm:flex-none">
<span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md"> <span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md">
<svg <FilterIcon className="w-6 h-6" />
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z"
clipRule="evenodd"
/>
</svg>
</span> </span>
<select <select
id="filter" id="filter"
@ -360,19 +335,7 @@ const SettingsLogs: React.FC = () => {
onClick={() => setActiveLog(row)} onClick={() => setActiveLog(row)}
className="mr-2" className="mr-2"
> >
<svg <DocumentSearchIcon className="w-5 h-5 text-white" />
className="w-5 h-5 text-white"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2h-1.528A6 6 0 004 9.528V4z" />
<path
fillRule="evenodd"
d="M8 10a4 4 0 00-3.446 6.032l-1.261 1.26a1 1 0 101.414 1.415l1.261-1.261A4 4 0 108 10zm-2 4a2 2 0 114 0 2 2 0 01-4 0z"
clipRule="evenodd"
/>
</svg>
</Button> </Button>
)} )}
<Button <Button
@ -380,15 +343,7 @@ const SettingsLogs: React.FC = () => {
buttonSize="sm" buttonSize="sm"
onClick={() => copyLogString(row)} onClick={() => copyLogString(row)}
> >
<svg <ClipboardCopyIcon className="w-5 h-5 text-white" />
className="w-5 h-5 text-white"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M8 2a1 1 0 000 2h2a1 1 0 100-2H8z" />
<path d="M3 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v6h-4.586l1.293-1.293a1 1 0 00-1.414-1.414l-3 3a1 1 0 000 1.414l3 3a1 1 0 001.414-1.414L10.414 13H15v3a2 2 0 01-2 2H5a2 2 0 01-2-2V5zM15 11h2a1 1 0 110 2h-2v-2z" />
</svg>
</Button> </Button>
</Table.TD> </Table.TD>
</tr> </tr>

@ -1,3 +1,4 @@
import { RefreshIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
@ -204,18 +205,7 @@ const SettingsMain: React.FC = () => {
}} }}
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 rounded-r-md hover:bg-indigo-500 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700" 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 rounded-r-md hover:bg-indigo-500 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700"
> >
<svg <RefreshIcon className="w-5 h-5" />
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
clipRule="evenodd"
/>
</svg>
</button> </button>
</div> </div>
</div> </div>

@ -1,6 +1,7 @@
import { AtSymbolIcon } from '@heroicons/react/outline';
import { LightningBoltIcon } from '@heroicons/react/solid';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import Bolt from '../../assets/bolt.svg';
import DiscordLogo from '../../assets/extlogos/discord.svg'; import DiscordLogo from '../../assets/extlogos/discord.svg';
import PushbulletLogo from '../../assets/extlogos/pushbullet.svg'; import PushbulletLogo from '../../assets/extlogos/pushbullet.svg';
import PushoverLogo from '../../assets/extlogos/pushover.svg'; import PushoverLogo from '../../assets/extlogos/pushover.svg';
@ -27,20 +28,7 @@ const SettingsNotifications: React.FC = ({ children }) => {
text: intl.formatMessage(messages.email), text: intl.formatMessage(messages.email),
content: ( content: (
<span className="flex items-center"> <span className="flex items-center">
<svg <AtSymbolIcon className="h-4 mr-2" />
className="h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"
/>
</svg>
{intl.formatMessage(messages.email)} {intl.formatMessage(messages.email)}
</span> </span>
), ),
@ -106,7 +94,7 @@ const SettingsNotifications: React.FC = ({ children }) => {
text: intl.formatMessage(messages.webhook), text: intl.formatMessage(messages.webhook),
content: ( content: (
<span className="flex items-center"> <span className="flex items-center">
<Bolt className="h-4 mr-2" /> <LightningBoltIcon className="h-4 mr-2" />
{intl.formatMessage(messages.webhook)} {intl.formatMessage(messages.webhook)}
</span> </span>
), ),

@ -1,5 +1,7 @@
import { RefreshIcon, SearchIcon, XIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import { Field, Formik } from 'formik'; import { Field, Formik } from 'formik';
import { orderBy } from 'lodash';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
@ -7,7 +9,6 @@ import useSWR from 'swr';
import * as Yup from 'yup'; import * as Yup from 'yup';
import type { PlexDevice } from '../../../server/interfaces/api/plexInterfaces'; import type { PlexDevice } from '../../../server/interfaces/api/plexInterfaces';
import type { PlexSettings } from '../../../server/lib/settings'; import type { PlexSettings } from '../../../server/lib/settings';
import Spinner from '../../assets/spinner.svg';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
import Alert from '../Common/Alert'; import Alert from '../Common/Alert';
import Badge from '../Common/Badge'; import Badge from '../Common/Badge';
@ -28,7 +29,7 @@ const messages = defineMessages({
serverpresetPlaceholder: 'Plex Server', serverpresetPlaceholder: 'Plex Server',
serverLocal: 'local', serverLocal: 'local',
serverRemote: 'remote', serverRemote: 'remote',
serverConnected: 'connected', serverSecure: 'secure',
serverpresetManualMessage: 'Manual configuration', serverpresetManualMessage: 'Manual configuration',
serverpresetRefreshing: 'Retrieving servers…', serverpresetRefreshing: 'Retrieving servers…',
serverpresetLoad: 'Press the button to load available servers', serverpresetLoad: 'Press the button to load available servers',
@ -131,7 +132,7 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
dev.connection.forEach((conn) => dev.connection.forEach((conn) =>
finalPresets.push({ finalPresets.push({
name: dev.name, name: dev.name,
ssl: !conn.local && conn.protocol === 'https', ssl: conn.protocol === 'https',
uri: conn.uri, uri: conn.uri,
address: conn.address, address: conn.address,
port: conn.port, port: conn.port,
@ -141,14 +142,8 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
}) })
); );
}); });
finalPresets.sort((a, b) => {
if (a.status && !b.status) { return orderBy(finalPresets, ['status', 'ssl'], ['desc', 'desc']);
return -1;
} else {
return 1;
}
});
return finalPresets;
}, [availableServers]); }, [availableServers]);
const syncLibraries = async () => { const syncLibraries = async () => {
@ -420,7 +415,13 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
server.local server.local
? intl.formatMessage(messages.serverLocal) ? intl.formatMessage(messages.serverLocal)
: intl.formatMessage(messages.serverRemote) : intl.formatMessage(messages.serverRemote)
}] }]${
server.ssl
? ` [${intl.formatMessage(
messages.serverSecure
)}]`
: ''
}
${server.status ? '' : '(' + server.message + ')'} ${server.status ? '' : '(' + server.message + ')'}
`} `}
</option> </option>
@ -433,22 +434,12 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
}} }}
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 rounded-r-md hover:bg-indigo-500 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700" 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 rounded-r-md hover:bg-indigo-500 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700"
> >
{isRefreshingPresets ? ( <RefreshIcon
<Spinner className="w-5 h-5" /> className={`w-5 h-5 ${
) : ( isRefreshingPresets ? 'animate-spin' : ''
<svg }`}
className="w-5 h-5" style={{ animationDirection: 'reverse' }}
fill="currentColor" />
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
clipRule="evenodd"
/>
</svg>
)}
</button> </button>
</div> </div>
</div> </div>
@ -538,18 +529,10 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
</div> </div>
<div className="section"> <div className="section">
<Button onClick={() => syncLibraries()} disabled={isSyncing}> <Button onClick={() => syncLibraries()} disabled={isSyncing}>
<svg <RefreshIcon
className={`${isSyncing ? 'animate-spin' : ''} w-5 h-5 mr-1`} className={`w-5 h-5 mr-1 ${isSyncing ? 'animate-spin' : ''}`}
fill="currentColor" style={{ animationDirection: 'reverse' }}
viewBox="0 0 20 20" />
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
clipRule="evenodd"
/>
</svg>
{isSyncing {isSyncing
? intl.formatMessage(messages.scanning) ? intl.formatMessage(messages.scanning)
: intl.formatMessage(messages.scan)} : intl.formatMessage(messages.scan)}
@ -623,40 +606,14 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
<div className="flex-1 text-right"> <div className="flex-1 text-right">
{!dataSync?.running && ( {!dataSync?.running && (
<Button buttonType="warning" onClick={() => startScan()}> <Button buttonType="warning" onClick={() => startScan()}>
<svg <SearchIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
{intl.formatMessage(messages.startscan)} {intl.formatMessage(messages.startscan)}
</Button> </Button>
)} )}
{dataSync?.running && ( {dataSync?.running && (
<Button buttonType="danger" onClick={() => cancelScan()}> <Button buttonType="danger" onClick={() => cancelScan()}>
<svg <XIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
{intl.formatMessage(messages.cancelscan)} {intl.formatMessage(messages.cancelscan)}
</Button> </Button>
)} )}

@ -1,3 +1,4 @@
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@ -130,17 +131,8 @@ const ServerInstance: React.FC<ServerInstanceProps> = ({
onClick={() => onEdit()} onClick={() => onEdit()}
className="relative inline-flex items-center justify-center flex-1 w-0 py-4 -mr-px text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out border border-transparent rounded-bl-lg hover:text-white focus:outline-none focus:ring-blue focus:border-gray-500 focus:z-10" className="relative inline-flex items-center justify-center flex-1 w-0 py-4 -mr-px text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out border border-transparent rounded-bl-lg hover:text-white focus:outline-none focus:ring-blue focus:border-gray-500 focus:z-10"
> >
<svg <PencilIcon className="w-5 h-5 mr-2" />
className="w-5 h-5" {intl.formatMessage(globalMessages.edit)}
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
<span className="ml-3">
{intl.formatMessage(globalMessages.edit)}
</span>
</button> </button>
</div> </div>
<div className="flex flex-1 w-0 -ml-px"> <div className="flex flex-1 w-0 -ml-px">
@ -148,21 +140,8 @@ const ServerInstance: React.FC<ServerInstanceProps> = ({
onClick={() => onDelete()} onClick={() => onDelete()}
className="relative inline-flex items-center justify-center flex-1 w-0 py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out border border-transparent rounded-br-lg hover:text-white focus:outline-none focus:ring-blue focus:border-gray-500 focus:z-10" className="relative inline-flex items-center justify-center flex-1 w-0 py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out border border-transparent rounded-br-lg hover:text-white focus:outline-none focus:ring-blue focus:border-gray-500 focus:z-10"
> >
<svg <TrashIcon className="w-5 h-5 mr-2" />
className="w-5 h-5" {intl.formatMessage(globalMessages.delete)}
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
<span className="ml-3">
{intl.formatMessage(globalMessages.delete)}
</span>
</button> </button>
</div> </div>
</div> </div>
@ -278,6 +257,7 @@ const SettingsServices: React.FC = () => {
}) })
} }
title="Delete Server" title="Delete Server"
iconSvg={<TrashIcon className="w-6 h-6" />}
> >
{intl.formatMessage(messages.deleteserverconfirm)} {intl.formatMessage(messages.deleteserverconfirm)}
</Modal> </Modal>
@ -343,18 +323,7 @@ const SettingsServices: React.FC = () => {
setEditRadarrModal({ open: true, radarr: null }) setEditRadarrModal({ open: true, radarr: null })
} }
> >
<svg <PlusIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clipRule="evenodd"
/>
</svg>
{intl.formatMessage(messages.addradarr)} {intl.formatMessage(messages.addradarr)}
</Button> </Button>
</div> </div>
@ -434,18 +403,7 @@ const SettingsServices: React.FC = () => {
setEditSonarrModal({ open: true, sonarr: null }) setEditSonarrModal({ open: true, sonarr: null })
} }
> >
<svg <PlusIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clipRule="evenodd"
/>
</svg>
{intl.formatMessage(messages.addsonarr)} {intl.formatMessage(messages.addsonarr)}
</Button> </Button>
</div> </div>

@ -1,3 +1,4 @@
import { PencilIcon, PlusIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import { Field, Formik } from 'formik'; import { Field, Formik } from 'formik';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
@ -66,7 +67,7 @@ const messages = defineMessages({
syncEnabled: 'Enable Scan', syncEnabled: 'Enable Scan',
externalUrl: 'External URL', externalUrl: 'External URL',
externalUrlPlaceholder: 'External URL pointing to your Sonarr server', externalUrlPlaceholder: 'External URL pointing to your Sonarr server',
preventSearch: 'Disable Auto-Search', enableSearch: 'Enable Automatic Search',
validationApplicationUrl: 'You must provide a valid URL', validationApplicationUrl: 'You must provide a valid URL',
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
validationBaseUrlLeadingSlash: 'Base URL must have a leading slash', validationBaseUrlLeadingSlash: 'Base URL must have a leading slash',
@ -273,7 +274,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
enableSeasonFolders: sonarr?.enableSeasonFolders ?? false, enableSeasonFolders: sonarr?.enableSeasonFolders ?? false,
externalUrl: sonarr?.externalUrl, externalUrl: sonarr?.externalUrl,
syncEnabled: sonarr?.syncEnabled ?? false, syncEnabled: sonarr?.syncEnabled ?? false,
preventSearch: sonarr?.preventSearch ?? false, enableSearch: !sonarr?.preventSearch,
}} }}
validationSchema={SonarrSettingsSchema} validationSchema={SonarrSettingsSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
@ -313,7 +314,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
enableSeasonFolders: values.enableSeasonFolders, enableSeasonFolders: values.enableSeasonFolders,
externalUrl: values.externalUrl, externalUrl: values.externalUrl,
syncEnabled: values.syncEnabled, syncEnabled: values.syncEnabled,
preventSearch: values.preventSearch, preventSearch: !values.enableSearch,
}; };
if (!sonarr) { if (!sonarr) {
await axios.post('/api/v1/settings/sonarr', submission); await axios.post('/api/v1/settings/sonarr', submission);
@ -387,6 +388,13 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
values.is4k ? messages.edit4ksonarr : messages.editsonarr values.is4k ? messages.edit4ksonarr : messages.editsonarr
) )
} }
iconSvg={
!sonarr ? (
<PlusIcon className="w-6 h-6" />
) : (
<PencilIcon className="w-6 h-6" />
)
}
> >
<div className="mb-6"> <div className="mb-6">
<div className="form-row"> <div className="form-row">
@ -953,14 +961,14 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
</div> </div>
</div> </div>
<div className="form-row"> <div className="form-row">
<label htmlFor="preventSearch" className="checkbox-label"> <label htmlFor="enableSearch" className="checkbox-label">
{intl.formatMessage(messages.preventSearch)} {intl.formatMessage(messages.enableSearch)}
</label> </label>
<div className="mt-1 sm:mt-0 sm:col-span-2"> <div className="form-input">
<Field <Field
type="checkbox" type="checkbox"
id="preventSearch" id="enableSearch"
name="preventSearch" name="enableSearch"
/> />
</div> </div>
</div> </div>

@ -1,3 +1,4 @@
import { CheckIcon } from '@heroicons/react/solid';
import React from 'react'; import React from 'react';
interface CurrentStep { interface CurrentStep {
@ -17,26 +18,13 @@ const SetupSteps: React.FC<CurrentStep> = ({
}) => { }) => {
return ( return (
<li className="relative md:flex-1 md:flex"> <li className="relative md:flex-1 md:flex">
<div className="px-6 py-4 flex items-center text-sm leading-5 font-medium space-x-4"> <div className="flex items-center px-6 py-4 space-x-4 text-sm font-medium leading-5">
<div <div
className={`flex-shrink-0 w-10 h-10 flex items-center justify-center border-2 className={`flex-shrink-0 w-10 h-10 flex items-center justify-center border-2
${active ? 'border-indigo-600 ' : 'border-white '} ${active ? 'border-indigo-600 ' : 'border-white '}
${completed ? 'bg-indigo-600 border-indigo-600 ' : ''} rounded-full`} ${completed ? 'bg-indigo-600 border-indigo-600 ' : ''} rounded-full`}
> >
{completed && ( {completed && <CheckIcon className="w-6 h-6 text-white" />}
<svg
className="w-6 h-6 text-white"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip="evenodd"
/>
</svg>
)}
{!completed && ( {!completed && (
<p className={active ? 'text-white' : 'text-indigo-200'}> <p className={active ? 'text-white' : 'text-indigo-200'}>
{stepNumber} {stepNumber}
@ -53,9 +41,9 @@ const SetupSteps: React.FC<CurrentStep> = ({
</div> </div>
{!isLastStep && ( {!isLastStep && (
<div className="hidden md:block absolute top-0 right-0 h-full w-5"> <div className="absolute top-0 right-0 hidden w-5 h-full md:block">
<svg <svg
className="h-full w-full text-gray-600" className="w-full h-full text-gray-600"
viewBox="0 0 22 80" viewBox="0 0 22 80"
fill="none" fill="none"
preserveAspectRatio="none" preserveAspectRatio="none"

@ -1,3 +1,4 @@
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/outline';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import React, { import React, {
ReactNode, ReactNode,
@ -166,20 +167,7 @@ const Slider: React.FC<SliderProps> = ({
onClick={() => slide(Direction.LEFT)} onClick={() => slide(Direction.LEFT)}
disabled={scrollPos.isStart} disabled={scrollPos.isStart}
> >
<svg <ChevronLeftIcon className="w-6 h-6" />
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</button> </button>
<button <button
className={`${ className={`${
@ -188,20 +176,7 @@ const Slider: React.FC<SliderProps> = ({
onClick={() => slide(Direction.RIGHT)} onClick={() => slide(Direction.RIGHT)}
disabled={scrollPos.isEnd} disabled={scrollPos.isEnd}
> >
<svg <ChevronRightIcon className="w-6 h-6" />
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</button> </button>
</div> </div>
<div <div

@ -1,3 +1,4 @@
import { SparklesIcon } from '@heroicons/react/outline';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
@ -38,22 +39,7 @@ const StatusChecker: React.FC = () => {
show={data.commitTag !== process.env.commitTag} show={data.commitTag !== process.env.commitTag}
> >
<Modal <Modal
iconSvg={ iconSvg={<SparklesIcon className="w-6 h-6" />}
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
/>
</svg>
}
title={intl.formatMessage(messages.newversionavailable)} title={intl.formatMessage(messages.newversionavailable)}
onOk={() => location.reload()} onOk={() => location.reload()}
okText={intl.formatMessage(messages.reloadOverseerr)} okText={intl.formatMessage(messages.reloadOverseerr)}

@ -1,3 +1,5 @@
import { DownloadIcon } from '@heroicons/react/outline';
import { BellIcon, CheckIcon, ClockIcon } from '@heroicons/react/solid';
import Link from 'next/link'; import Link from 'next/link';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@ -128,32 +130,12 @@ const TitleCard: React.FC<TitleCardProps> = ({
{(currentStatus === MediaStatus.AVAILABLE || {(currentStatus === MediaStatus.AVAILABLE ||
currentStatus === MediaStatus.PARTIALLY_AVAILABLE) && ( currentStatus === MediaStatus.PARTIALLY_AVAILABLE) && (
<div className="flex items-center justify-center w-4 h-4 text-white bg-green-400 rounded-full shadow sm:w-5 sm:h-5"> <div className="flex items-center justify-center w-4 h-4 text-white bg-green-400 rounded-full shadow sm:w-5 sm:h-5">
<svg <CheckIcon className="w-3 h-3 sm:w-4 sm:h-4" />
className="w-3 h-3 sm:w-4 sm:h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div> </div>
)} )}
{currentStatus === MediaStatus.PENDING && ( {currentStatus === MediaStatus.PENDING && (
<div className="flex items-center justify-center w-4 h-4 text-white bg-yellow-500 rounded-full shadow sm:w-5 sm:h-5"> <div className="flex items-center justify-center w-4 h-4 text-white bg-yellow-500 rounded-full shadow sm:w-5 sm:h-5">
<svg <BellIcon className="w-3 h-3 sm:w-4 sm:h-4" />
className="w-3 h-3 sm:w-4 sm:h-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z" />
</svg>
</div> </div>
)} )}
{currentStatus === MediaStatus.PROCESSING && ( {currentStatus === MediaStatus.PROCESSING && (
@ -161,18 +143,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
{inProgress ? ( {inProgress ? (
<Spinner className="w-3 h-3" /> <Spinner className="w-3 h-3" />
) : ( ) : (
<svg <ClockIcon className="w-3 h-3 sm:w-4 sm:h-4" />
className="w-3 h-3 sm:w-4 sm:h-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
clipRule="evenodd"
/>
</svg>
)} )}
</div> </div>
)} )}
@ -265,20 +236,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
}} }}
className="flex items-center justify-center w-full text-white transition duration-150 ease-in-out bg-indigo-600 rounded-md h-7 hover:bg-indigo-500 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700" className="flex items-center justify-center w-full text-white transition duration-150 ease-in-out bg-indigo-600 rounded-md h-7 hover:bg-indigo-500 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700"
> >
<svg <DownloadIcon className="w-4 h-4 mr-1" />
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
<span className="text-xs"> <span className="text-xs">
{intl.formatMessage(globalMessages.request)} {intl.formatMessage(globalMessages.request)}
</span> </span>

@ -1,98 +1,41 @@
import {
CheckCircleIcon,
ExclamationCircleIcon,
ExclamationIcon,
InformationCircleIcon,
} from '@heroicons/react/outline';
import { XIcon } from '@heroicons/react/solid';
import React from 'react'; import React from 'react';
import type { ToastProps } from 'react-toast-notifications'; import type { ToastProps } from 'react-toast-notifications';
const Toast: React.FC<ToastProps> = ({ appearance, children, onDismiss }) => { const Toast: React.FC<ToastProps> = ({ appearance, children, onDismiss }) => {
return ( return (
<div className="toast flex items-end justify-center px-2 py-2 pointer-events-none sm:items-start sm:justify-end"> <div className="flex items-end justify-center px-2 py-2 pointer-events-none toast sm:items-start sm:justify-end">
<div className="max-w-sm w-full bg-gray-700 shadow-lg rounded-lg pointer-events-auto"> <div className="w-full max-w-sm bg-gray-700 rounded-lg shadow-lg pointer-events-auto">
<div className="rounded-lg ring-1 ring-black ring-opacity-5 overflow-hidden"> <div className="overflow-hidden rounded-lg ring-1 ring-black ring-opacity-5">
<div className="p-4"> <div className="p-4">
<div className="flex items-start"> <div className="flex items-start">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
{appearance === 'success' && ( {appearance === 'success' && (
<svg <CheckCircleIcon className="w-6 h-6 text-green-400" />
className="h-6 w-6 text-green-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
)} )}
{appearance === 'error' && ( {appearance === 'error' && (
<svg <ExclamationCircleIcon className="w-6 h-6 text-red-500" />
className="w-6 h-6 text-red-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
)} )}
{appearance === 'info' && ( {appearance === 'info' && (
<svg <InformationCircleIcon className="w-6 h-6 text-indigo-500" />
className="w-6 h-6 text-indigo-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
)} )}
{appearance === 'warning' && ( {appearance === 'warning' && (
<svg <ExclamationIcon className="w-6 h-6 text-orange-400" />
className="w-6 h-6 text-orange-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
)} )}
</div> </div>
<div className="ml-3 w-0 flex-1 text-white">{children}</div> <div className="flex-1 w-0 ml-3 text-white">{children}</div>
<div className="ml-4 flex-shrink-0 flex"> <div className="flex flex-shrink-0 ml-4">
<button <button
onClick={() => onDismiss()} onClick={() => onDismiss()}
className="inline-flex text-gray-400 focus:outline-none focus:text-gray-500 transition ease-in-out duration-150" className="inline-flex text-gray-400 transition duration-150 ease-in-out focus:outline-none focus:text-gray-500"
> >
<svg <XIcon className="w-5 h-5" />
className="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button> </button>
</div> </div>
</div> </div>

@ -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 axios from 'axios';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@ -50,7 +61,7 @@ const messages = defineMessages({
manageModalTitle: 'Manage Series', manageModalTitle: 'Manage Series',
manageModalRequests: 'Requests', manageModalRequests: 'Requests',
manageModalNoRequests: 'No requests.', manageModalNoRequests: 'No requests.',
manageModalClearMedia: 'Clear All Media Data', manageModalClearMedia: 'Clear Media Data',
manageModalClearMediaWarning: 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.', '* 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.',
originaltitle: 'Original Title', originaltitle: 'Original Title',
@ -113,6 +124,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
mediaLinks.push({ mediaLinks.push({
text: intl.formatMessage(messages.playonplex), text: intl.formatMessage(messages.playonplex),
url: data.mediaInfo?.plexUrl, url: data.mediaInfo?.plexUrl,
svg: <PlayIcon className="w-5 h-5 mr-1" />,
}); });
} }
@ -125,6 +137,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
mediaLinks.push({ mediaLinks.push({
text: intl.formatMessage(messages.play4konplex), text: intl.formatMessage(messages.play4konplex),
url: data.mediaInfo?.plexUrl4k, url: data.mediaInfo?.plexUrl4k,
svg: <PlayIcon className="w-5 h-5 mr-1" />,
}); });
} }
@ -137,6 +150,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
mediaLinks.push({ mediaLinks.push({
text: intl.formatMessage(messages.watchtrailer), text: intl.formatMessage(messages.watchtrailer),
url: trailerUrl, url: trailerUrl,
svg: <FilmIcon className="w-5 h-5 mr-1" />,
}); });
} }
@ -302,18 +316,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
className="w-full sm:mb-0" className="w-full sm:mb-0"
buttonType="success" buttonType="success"
> >
<svg <CheckCircleIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z"
clipRule="evenodd"
/>
</svg>
<span>{intl.formatMessage(messages.markavailable)}</span> <span>{intl.formatMessage(messages.markavailable)}</span>
</Button> </Button>
</div> </div>
@ -327,18 +330,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
className="w-full sm:mb-0" className="w-full sm:mb-0"
buttonType="success" buttonType="success"
> >
<svg <CheckCircleIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z"
clipRule="evenodd"
/>
</svg>
<span> <span>
{intl.formatMessage(messages.mark4kavailable)} {intl.formatMessage(messages.mark4kavailable)}
</span> </span>
@ -380,15 +372,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
className="block mb-2 last:mb-0" className="block mb-2 last:mb-0"
> >
<Button buttonType="ghost" className="w-full"> <Button buttonType="ghost" className="w-full">
<svg <ExternalLinkIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
<span>{intl.formatMessage(messages.opensonarr)}</span> <span>{intl.formatMessage(messages.opensonarr)}</span>
</Button> </Button>
</a> </a>
@ -400,15 +384,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
rel="noreferrer" rel="noreferrer"
> >
<Button buttonType="ghost" className="w-full"> <Button buttonType="ghost" className="w-full">
<svg <ExternalLinkIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
<span>{intl.formatMessage(messages.opensonarr4k)}</span> <span>{intl.formatMessage(messages.opensonarr4k)}</span>
</Button> </Button>
</a> </a>
@ -422,6 +398,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
confirmText={intl.formatMessage(globalMessages.areyousure)} confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full" className="w-full"
> >
<DocumentRemoveIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.manageModalClearMedia)} {intl.formatMessage(messages.manageModalClearMedia)}
</ConfirmButton> </ConfirmButton>
<div className="mt-2 text-sm text-gray-400"> <div className="mt-2 text-sm text-gray-400">
@ -501,26 +478,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
className="ml-2 first:ml-0" className="ml-2 first:ml-0"
onClick={() => setShowManager(true)} onClick={() => setShowManager(true)}
> >
<svg <CogIcon className="w-5" />
className="w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</Button> </Button>
)} )}
</div> </div>
@ -564,20 +522,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
<Link href={`/tv/${data.id}/crew`}> <Link href={`/tv/${data.id}/crew`}>
<a className="flex items-center text-gray-400 transition duration-300 hover:text-gray-100"> <a className="flex items-center text-gray-400 transition duration-300 hover:text-gray-100">
<span>{intl.formatMessage(messages.viewfullcrew)}</span> <span>{intl.formatMessage(messages.viewfullcrew)}</span>
<svg <ArrowCircleRightIcon className="inline-block w-5 h-5 ml-1" />
className="inline-block w-5 h-5 ml-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a> </a>
</Link> </Link>
</div> </div>
@ -734,20 +679,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
<Link href="/tv/[tvId]/cast" as={`/tv/${data.id}/cast`}> <Link href="/tv/[tvId]/cast" as={`/tv/${data.id}/cast`}>
<a className="slider-title"> <a className="slider-title">
<span>{intl.formatMessage(messages.cast)}</span> <span>{intl.formatMessage(messages.cast)}</span>
<svg <ArrowCircleRightIcon className="w-6 h-6 ml-2" />
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a> </a>
</Link> </Link>
</div> </div>

@ -1,3 +1,4 @@
import { PencilIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@ -84,6 +85,7 @@ const BulkEditModal: React.FC<BulkEditProps> = ({
return ( return (
<Modal <Modal
title={intl.formatMessage(messages.edituser)} title={intl.formatMessage(messages.edituser)}
iconSvg={<PencilIcon className="w-6 h-6" />}
onOk={() => { onOk={() => {
updateUsers(); updateUsers();
}} }}

@ -1,3 +1,10 @@
import { TrashIcon } from '@heroicons/react/outline';
import {
InboxInIcon,
PencilIcon,
SortDescendingIcon,
UserAddIcon,
} from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import Link from 'next/link'; import Link from 'next/link';
@ -10,7 +17,6 @@ import * as Yup from 'yup';
import type { UserResultsResponse } from '../../../server/interfaces/api/userInterfaces'; import type { UserResultsResponse } from '../../../server/interfaces/api/userInterfaces';
import { UserSettingsNotificationsResponse } from '../../../server/interfaces/api/userSettingsInterfaces'; import { UserSettingsNotificationsResponse } from '../../../server/interfaces/api/userSettingsInterfaces';
import { hasPermission } from '../../../server/lib/permissions'; import { hasPermission } from '../../../server/lib/permissions';
import AddUserIcon from '../../assets/useradd.svg';
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams'; import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
import { Permission, User, UserType, useUser } from '../../hooks/useUser'; import { Permission, User, UserType, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
@ -56,6 +62,8 @@ const messages = defineMessages({
validationpasswordminchars: validationpasswordminchars:
'Password is too short; should be a minimum of 8 characters', 'Password is too short; should be a minimum of 8 characters',
usercreatedfailed: 'Something went wrong while creating the user.', usercreatedfailed: 'Something went wrong while creating the user.',
usercreatedfailedexisting:
'The provided email address is already in use by another user.',
usercreatedsuccess: 'User created successfully!', usercreatedsuccess: 'User created successfully!',
email: 'Email Address', email: 'Email Address',
password: 'Password', password: 'Password',
@ -265,22 +273,7 @@ const UserList: React.FC = () => {
okButtonType="danger" okButtonType="danger"
onCancel={() => setDeleteModal({ isOpen: false })} onCancel={() => setDeleteModal({ isOpen: false })}
title={intl.formatMessage(messages.deleteuser)} title={intl.formatMessage(messages.deleteuser)}
iconSvg={ iconSvg={<TrashIcon className="w-6 h-6" />}
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
}
> >
{intl.formatMessage(messages.deleteconfirm)} {intl.formatMessage(messages.deleteconfirm)}
</Modal> </Modal>
@ -314,10 +307,17 @@ const UserList: React.FC = () => {
}); });
setCreateModal({ isOpen: false }); setCreateModal({ isOpen: false });
} catch (e) { } catch (e) {
addToast(intl.formatMessage(messages.usercreatedfailed), { addToast(
appearance: 'error', intl.formatMessage(
autoDismiss: true, e.response.data.errors?.includes('USER_EXISTS')
}); ? messages.usercreatedfailedexisting
: messages.usercreatedfailed
),
{
appearance: 'error',
autoDismiss: true,
}
);
} finally { } finally {
revalidate(); revalidate();
} }
@ -335,7 +335,7 @@ const UserList: React.FC = () => {
return ( return (
<Modal <Modal
title={intl.formatMessage(messages.createuser)} title={intl.formatMessage(messages.createuser)}
iconSvg={<AddUserIcon className="h-6" />} iconSvg={<UserAddIcon className="w-6 h-6" />}
onOk={() => handleSubmit()} onOk={() => handleSubmit()}
okText={ okText={
isSubmitting isSubmitting
@ -445,12 +445,13 @@ const UserList: React.FC = () => {
<div className="flex flex-col justify-between lg:items-end lg:flex-row"> <div className="flex flex-col justify-between lg:items-end lg:flex-row">
<Header>{intl.formatMessage(messages.userlist)}</Header> <Header>{intl.formatMessage(messages.userlist)}</Header>
<div className="flex flex-col flex-grow mt-2 lg:flex-row lg:flex-grow-0"> <div className="flex flex-col flex-grow mt-2 lg:flex-row lg:flex-grow-0">
<div className="flex flex-row justify-between flex-grow mb-2 lg:mb-0 lg:flex-grow-0"> <div className="flex flex-col justify-between flex-grow mb-2 sm:flex-row lg:mb-0 lg:flex-grow-0">
<Button <Button
className="flex-grow mr-2 outline" className="flex-grow mb-2 sm:mb-0 sm:mr-2 outline"
buttonType="primary" buttonType="primary"
onClick={() => setCreateModal({ isOpen: true })} onClick={() => setCreateModal({ isOpen: true })}
> >
<UserAddIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.createlocaluser)} {intl.formatMessage(messages.createlocaluser)}
</Button> </Button>
<Button <Button
@ -459,19 +460,13 @@ const UserList: React.FC = () => {
disabled={isImporting} disabled={isImporting}
onClick={() => importFromPlex()} onClick={() => importFromPlex()}
> >
<InboxInIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.importfromplex)} {intl.formatMessage(messages.importfromplex)}
</Button> </Button>
</div> </div>
<div className="flex flex-grow mb-2 lg:mb-0 lg:flex-grow-0"> <div className="flex flex-grow mb-2 lg:mb-0 lg:flex-grow-0">
<span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md"> <span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md">
<svg <SortDescendingIcon className="w-6 h-6" />
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3 3a1 1 0 000 2h11a1 1 0 100-2H3zM3 7a1 1 0 000 2h7a1 1 0 100-2H3zM3 11a1 1 0 100 2h4a1 1 0 100-2H3zM15 8a1 1 0 10-2 0v5.586l-1.293-1.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L15 13.586V8z" />
</svg>
</span> </span>
<select <select
id="sort" id="sort"
@ -528,6 +523,7 @@ const UserList: React.FC = () => {
onClick={() => setShowBulkEditModal(true)} onClick={() => setShowBulkEditModal(true)}
disabled={selectedUsers.length === 0} disabled={selectedUsers.length === 0}
> >
<PencilIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.bulkedit)} {intl.formatMessage(messages.bulkedit)}
</Button> </Button>
)} )}

@ -1,3 +1,4 @@
import { CogIcon, UserIcon } from '@heroicons/react/solid';
import Link from 'next/link'; import Link from 'next/link';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@ -92,18 +93,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
passHref passHref
> >
<Button as="a"> <Button as="a">
<svg <CogIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
clipRule="evenodd"
/>
</svg>
{intl.formatMessage(messages.settings)} {intl.formatMessage(messages.settings)}
</Button> </Button>
</Link> </Link>
@ -116,18 +106,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
passHref passHref
> >
<Button as="a"> <Button as="a">
<svg <UserIcon className="w-5 h-5 mr-1" />
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clipRule="evenodd"
/>
</svg>
{intl.formatMessage(messages.profile)} {intl.formatMessage(messages.profile)}
</Button> </Button>
</Link> </Link>

@ -1,3 +1,4 @@
import { AtSymbolIcon } from '@heroicons/react/outline';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@ -33,20 +34,7 @@ const UserNotificationSettings: React.FC = ({ children }) => {
text: intl.formatMessage(messages.email), text: intl.formatMessage(messages.email),
content: ( content: (
<span className="flex items-center"> <span className="flex items-center">
<svg <AtSymbolIcon className="h-4 mr-2" />
className="h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"
/>
</svg>
{intl.formatMessage(messages.email)} {intl.formatMessage(messages.email)}
</span> </span>
), ),

@ -1,3 +1,4 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
@ -236,20 +237,7 @@ const UserProfile: React.FC = () => {
<Link href={`/users/${user?.id}/requests?filter=all`}> <Link href={`/users/${user?.id}/requests?filter=all`}>
<a className="slider-title"> <a className="slider-title">
<span>{intl.formatMessage(messages.recentrequests)}</span> <span>{intl.formatMessage(messages.recentrequests)}</span>
<svg <ArrowCircleRightIcon className="w-6 h-6 ml-2" />
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a> </a>
</Link> </Link>
</div> </div>

@ -9,7 +9,7 @@ const globalMessages = defineMessages({
requested: 'Requested', requested: 'Requested',
requesting: 'Requesting…', requesting: 'Requesting…',
request: 'Request', request: 'Request',
request4k: 'Request 4K', request4k: 'Request in 4K',
failed: 'Failed', failed: 'Failed',
pending: 'Pending', pending: 'Pending',
declined: 'Declined', declined: 'Declined',
@ -24,6 +24,7 @@ const globalMessages = defineMessages({
decline: 'Decline', decline: 'Decline',
delete: 'Delete', delete: 'Delete',
retry: 'Retry', retry: 'Retry',
retrying: 'Retrying…',
view: 'View', view: 'View',
deleting: 'Deleting…', deleting: 'Deleting…',
test: 'Test', test: 'Test',

@ -52,7 +52,7 @@
"components.Login.loginerror": "Something went wrong while trying to sign in.", "components.Login.loginerror": "Something went wrong while trying to sign in.",
"components.Login.password": "Password", "components.Login.password": "Password",
"components.Login.signin": "Sign In", "components.Login.signin": "Sign In",
"components.Login.signingin": "Signing in…", "components.Login.signingin": "Signing In…",
"components.Login.signinheader": "Sign in to continue", "components.Login.signinheader": "Sign in to continue",
"components.Login.signinwithoverseerr": "Use your {applicationTitle} account", "components.Login.signinwithoverseerr": "Use your {applicationTitle} account",
"components.Login.signinwithplex": "Use your Plex account", "components.Login.signinwithplex": "Use your Plex account",
@ -64,7 +64,7 @@
"components.MovieDetails.budget": "Budget", "components.MovieDetails.budget": "Budget",
"components.MovieDetails.cast": "Cast", "components.MovieDetails.cast": "Cast",
"components.MovieDetails.downloadstatus": "Download Status", "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.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.manageModalNoRequests": "No requests.",
"components.MovieDetails.manageModalRequests": "Requests", "components.MovieDetails.manageModalRequests": "Requests",
@ -140,7 +140,7 @@
"components.PersonDetails.birthdate": "Born {birthdate}", "components.PersonDetails.birthdate": "Born {birthdate}",
"components.PersonDetails.crewmember": "Crew", "components.PersonDetails.crewmember": "Crew",
"components.PersonDetails.lifespan": "{birthdate} {deathdate}", "components.PersonDetails.lifespan": "{birthdate} {deathdate}",
"components.PlexLoginButton.signingin": "Signing in…", "components.PlexLoginButton.signingin": "Signing In…",
"components.PlexLoginButton.signinwithplex": "Sign In", "components.PlexLoginButton.signinwithplex": "Sign In",
"components.QuotaSelector.movieRequestLimit": "{quotaLimit} movie(s) per {quotaDays} day(s)", "components.QuotaSelector.movieRequestLimit": "{quotaLimit} movie(s) per {quotaDays} day(s)",
"components.QuotaSelector.tvRequestLimit": "{quotaLimit} season(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.rootfolder": "Root Folder",
"components.RequestBlock.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", "components.RequestBlock.seasons": "{seasonCount, plural, one {Season} other {Seasons}}",
"components.RequestBlock.server": "Destination Server", "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.approverequest": "Approve Request",
"components.RequestButton.approverequest4k": "Approve 4K Request", "components.RequestButton.approverequest4k": "Approve 4K Request",
"components.RequestButton.approverequests": "Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}", "components.RequestButton.approverequests": "Approve {requestCount, plural, one {Request} other {{requestCount} Requests}}",
"components.RequestButton.decline4krequests": "Decline {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}", "components.RequestButton.decline4krequests": "Decline {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}",
"components.RequestButton.declinerequest": "Decline Request", "components.RequestButton.declinerequest": "Decline Request",
"components.RequestButton.declinerequest4k": "Decline 4K 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.requestmore": "Request More",
"components.RequestButton.requestmore4k": "Request More 4K", "components.RequestButton.requestmore4k": "Request More in 4K",
"components.RequestButton.viewrequest": "View Request", "components.RequestButton.viewrequest": "View Request",
"components.RequestButton.viewrequest4k": "View 4K Request", "components.RequestButton.viewrequest4k": "View 4K Request",
"components.RequestCard.deleterequest": "Delete Request", "components.RequestCard.deleterequest": "Delete Request",
@ -169,6 +169,7 @@
"components.RequestCard.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", "components.RequestCard.seasons": "{seasonCount, plural, one {Season} other {Seasons}}",
"components.RequestList.RequestItem.cancelRequest": "Cancel Request", "components.RequestList.RequestItem.cancelRequest": "Cancel Request",
"components.RequestList.RequestItem.deleterequest": "Delete 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.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.mediaerror": "The associated title for this request is no longer available.",
"components.RequestList.RequestItem.modified": "Modified", "components.RequestList.RequestItem.modified": "Modified",
@ -208,13 +209,13 @@
"components.RequestModal.alreadyrequested": "Already Requested", "components.RequestModal.alreadyrequested": "Already Requested",
"components.RequestModal.autoapproval": "Automatic Approval", "components.RequestModal.autoapproval": "Automatic Approval",
"components.RequestModal.cancel": "Cancel Request", "components.RequestModal.cancel": "Cancel Request",
"components.RequestModal.edit": "Edit Request",
"components.RequestModal.errorediting": "Something went wrong while editing the request.", "components.RequestModal.errorediting": "Something went wrong while editing the request.",
"components.RequestModal.extras": "Extras", "components.RequestModal.extras": "Extras",
"components.RequestModal.numberofepisodes": "# of Episodes", "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.pendingapproval": "Your request is pending approval.",
"components.RequestModal.pendingrequest": "Pending Request for {title}", "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.request4ktitle": "Request {title} in 4K",
"components.RequestModal.requestCancel": "Request for <strong>{title}</strong> canceled.", "components.RequestModal.requestCancel": "Request for <strong>{title}</strong> canceled.",
"components.RequestModal.requestSuccess": "<strong>{title}</strong> requested successfully!", "components.RequestModal.requestSuccess": "<strong>{title}</strong> requested successfully!",
@ -223,7 +224,7 @@
"components.RequestModal.requestcancelled": "Request for <strong>{title}</strong> canceled.", "components.RequestModal.requestcancelled": "Request for <strong>{title}</strong> canceled.",
"components.RequestModal.requestedited": "Request for <strong>{title}</strong> edited successfully!", "components.RequestModal.requestedited": "Request for <strong>{title}</strong> edited successfully!",
"components.RequestModal.requesterror": "Something went wrong while submitting the request.", "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.requestseasons": "Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}",
"components.RequestModal.requesttitle": "Request {title}", "components.RequestModal.requesttitle": "Request {title}",
"components.RequestModal.season": "Season", "components.RequestModal.season": "Season",
@ -290,11 +291,13 @@
"components.Settings.Notifications.chatId": "Chat ID", "components.Settings.Notifications.chatId": "Chat ID",
"components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.", "components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.",
"components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved successfully!", "components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved successfully!",
"components.Settings.Notifications.discordtestsent": "Discord test notification sent!",
"components.Settings.Notifications.emailNotificationTypesAlertDescription": "<strong>Media Requested</strong>, <strong>Media Automatically Approved</strong>, and <strong>Media Failed</strong> email notifications are sent to all users with the <strong>Manage Requests</strong> permission.", "components.Settings.Notifications.emailNotificationTypesAlertDescription": "<strong>Media Requested</strong>, <strong>Media Automatically Approved</strong>, and <strong>Media Failed</strong> email notifications are sent to all users with the <strong>Manage Requests</strong> permission.",
"components.Settings.Notifications.emailNotificationTypesAlertDescriptionPt2": "<strong>Media Approved</strong>, <strong>Media Declined</strong>, and <strong>Media Available</strong> email notifications are sent to the user who submitted the request.", "components.Settings.Notifications.emailNotificationTypesAlertDescriptionPt2": "<strong>Media Approved</strong>, <strong>Media Declined</strong>, and <strong>Media Available</strong> email notifications are sent to the user who submitted the request.",
"components.Settings.Notifications.emailsender": "Sender Address", "components.Settings.Notifications.emailsender": "Sender Address",
"components.Settings.Notifications.emailsettingsfailed": "Email notification settings failed to save.", "components.Settings.Notifications.emailsettingsfailed": "Email notification settings failed to save.",
"components.Settings.Notifications.emailsettingssaved": "Email notification settings saved successfully!", "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.enableSsl": "Enable SSL",
"components.Settings.Notifications.pgpPassword": "PGP Password", "components.Settings.Notifications.pgpPassword": "PGP Password",
"components.Settings.Notifications.pgpPasswordTip": "Sign encrypted email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>", "components.Settings.Notifications.pgpPasswordTip": "Sign encrypted email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>",
@ -309,7 +312,7 @@
"components.Settings.Notifications.ssldisabletip": "SSL should be disabled on standard TLS connections (port 587)", "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.telegramsettingsfailed": "Telegram notification settings failed to save.",
"components.Settings.Notifications.telegramsettingssaved": "Telegram notification settings saved successfully!", "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.validationBotAPIRequired": "You must provide a bot authentication token",
"components.Settings.Notifications.validationChatIdRequired": "You must provide a valid chat ID", "components.Settings.Notifications.validationChatIdRequired": "You must provide a valid chat ID",
"components.Settings.Notifications.validationEmail": "You must provide a valid email address", "components.Settings.Notifications.validationEmail": "You must provide a valid email address",
@ -331,6 +334,7 @@
"components.Settings.RadarrModal.defaultserver": "Default Server", "components.Settings.RadarrModal.defaultserver": "Default Server",
"components.Settings.RadarrModal.edit4kradarr": "Edit 4K Radarr Server", "components.Settings.RadarrModal.edit4kradarr": "Edit 4K Radarr Server",
"components.Settings.RadarrModal.editradarr": "Edit 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.externalUrl": "External URL",
"components.Settings.RadarrModal.externalUrlPlaceholder": "External URL pointing to your Radarr server", "components.Settings.RadarrModal.externalUrlPlaceholder": "External URL pointing to your Radarr server",
"components.Settings.RadarrModal.hostname": "Hostname or IP Address", "components.Settings.RadarrModal.hostname": "Hostname or IP Address",
@ -340,7 +344,6 @@
"components.Settings.RadarrModal.minimumAvailability": "Minimum Availability", "components.Settings.RadarrModal.minimumAvailability": "Minimum Availability",
"components.Settings.RadarrModal.notagoptions": "No tags.", "components.Settings.RadarrModal.notagoptions": "No tags.",
"components.Settings.RadarrModal.port": "Port", "components.Settings.RadarrModal.port": "Port",
"components.Settings.RadarrModal.preventSearch": "Disable Auto-Search",
"components.Settings.RadarrModal.qualityprofile": "Quality Profile", "components.Settings.RadarrModal.qualityprofile": "Quality Profile",
"components.Settings.RadarrModal.rootfolder": "Root Folder", "components.Settings.RadarrModal.rootfolder": "Root Folder",
"components.Settings.RadarrModal.selectMinimumAvailability": "Select minimum availability", "components.Settings.RadarrModal.selectMinimumAvailability": "Select minimum availability",
@ -461,6 +464,7 @@
"components.Settings.SonarrModal.defaultserver": "Default Server", "components.Settings.SonarrModal.defaultserver": "Default Server",
"components.Settings.SonarrModal.edit4ksonarr": "Edit 4K Sonarr Server", "components.Settings.SonarrModal.edit4ksonarr": "Edit 4K Sonarr Server",
"components.Settings.SonarrModal.editsonarr": "Edit 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.externalUrl": "External URL",
"components.Settings.SonarrModal.externalUrlPlaceholder": "External URL pointing to your Sonarr server", "components.Settings.SonarrModal.externalUrlPlaceholder": "External URL pointing to your Sonarr server",
"components.Settings.SonarrModal.hostname": "Hostname or IP Address", "components.Settings.SonarrModal.hostname": "Hostname or IP Address",
@ -471,7 +475,6 @@
"components.Settings.SonarrModal.loadingrootfolders": "Loading root folders…", "components.Settings.SonarrModal.loadingrootfolders": "Loading root folders…",
"components.Settings.SonarrModal.notagoptions": "No tags.", "components.Settings.SonarrModal.notagoptions": "No tags.",
"components.Settings.SonarrModal.port": "Port", "components.Settings.SonarrModal.port": "Port",
"components.Settings.SonarrModal.preventSearch": "Disable Auto-Search",
"components.Settings.SonarrModal.qualityprofile": "Quality Profile", "components.Settings.SonarrModal.qualityprofile": "Quality Profile",
"components.Settings.SonarrModal.rootfolder": "Root Folder", "components.Settings.SonarrModal.rootfolder": "Root Folder",
"components.Settings.SonarrModal.seasonfolders": "Season Folders", "components.Settings.SonarrModal.seasonfolders": "Season Folders",
@ -560,9 +563,9 @@
"components.Settings.regionTip": "Filter content by regional availability", "components.Settings.regionTip": "Filter content by regional availability",
"components.Settings.scan": "Sync Libraries", "components.Settings.scan": "Sync Libraries",
"components.Settings.scanning": "Syncing…", "components.Settings.scanning": "Syncing…",
"components.Settings.serverConnected": "connected",
"components.Settings.serverLocal": "local", "components.Settings.serverLocal": "local",
"components.Settings.serverRemote": "remote", "components.Settings.serverRemote": "remote",
"components.Settings.serverSecure": "secure",
"components.Settings.servername": "Server Name", "components.Settings.servername": "Server Name",
"components.Settings.servernamePlaceholder": "Plex Server Name", "components.Settings.servernamePlaceholder": "Plex Server Name",
"components.Settings.servernameTip": "Automatically retrieved from Plex after saving", "components.Settings.servernameTip": "Automatically retrieved from Plex after saving",
@ -620,7 +623,7 @@
"components.TvDetails.episodeRuntime": "Episode Runtime", "components.TvDetails.episodeRuntime": "Episode Runtime",
"components.TvDetails.episodeRuntimeMinutes": "{runtime} minutes", "components.TvDetails.episodeRuntimeMinutes": "{runtime} minutes",
"components.TvDetails.firstAirDate": "First Air Date", "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.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.manageModalNoRequests": "No requests.",
"components.TvDetails.manageModalRequests": "Requests", "components.TvDetails.manageModalRequests": "Requests",
@ -675,6 +678,7 @@
"components.UserList.totalrequests": "Total Requests", "components.UserList.totalrequests": "Total Requests",
"components.UserList.user": "User", "components.UserList.user": "User",
"components.UserList.usercreatedfailed": "Something went wrong while creating the 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.usercreatedsuccess": "User created successfully!",
"components.UserList.userdeleted": "User deleted successfully!", "components.UserList.userdeleted": "User deleted successfully!",
"components.UserList.userdeleteerror": "Something went wrong while deleting the user.", "components.UserList.userdeleteerror": "Something went wrong while deleting the user.",
@ -794,11 +798,12 @@
"i18n.previous": "Previous", "i18n.previous": "Previous",
"i18n.processing": "Processing", "i18n.processing": "Processing",
"i18n.request": "Request", "i18n.request": "Request",
"i18n.request4k": "Request 4K", "i18n.request4k": "Request in 4K",
"i18n.requested": "Requested", "i18n.requested": "Requested",
"i18n.requesting": "Requesting…", "i18n.requesting": "Requesting…",
"i18n.resultsperpage": "Display {pageSize} results per page", "i18n.resultsperpage": "Display {pageSize} results per page",
"i18n.retry": "Retry", "i18n.retry": "Retry",
"i18n.retrying": "Retrying…",
"i18n.save": "Save Changes", "i18n.save": "Save Changes",
"i18n.saving": "Saving…", "i18n.saving": "Saving…",
"i18n.settings": "Settings", "i18n.settings": "Settings",

@ -1,3 +1,4 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link'; import Link from 'next/link';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@ -22,22 +23,9 @@ const Custom404: React.FC = () => {
})} })}
</div> </div>
<Link href="/"> <Link href="/">
<a className="flex"> <a className="flex mt-2">
{intl.formatMessage(messages.returnHome)} {intl.formatMessage(messages.returnHome)}
<svg <ArrowCircleRightIcon className="w-6 h-6 ml-2" />
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a> </a>
</Link> </Link>
</div> </div>

@ -1,9 +1,10 @@
import React from 'react'; import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Link from 'next/link'; import Link from 'next/link';
import type { Undefinable } from '../utils/typeHelpers'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import PageTitle from '../components/Common/PageTitle'; import PageTitle from '../components/Common/PageTitle';
import type { Undefinable } from '../utils/typeHelpers';
interface ErrorProps { interface ErrorProps {
statusCode?: number; statusCode?: number;
@ -45,22 +46,9 @@ const Error: NextPage<ErrorProps> = ({ statusCode }) => {
: getErrorMessage(statusCode)} : getErrorMessage(statusCode)}
</div> </div>
<Link href="/"> <Link href="/">
<a className="flex"> <a className="flex mt-2">
{intl.formatMessage(messages.returnHome)} {intl.formatMessage(messages.returnHome)}
<svg <ArrowCircleRightIcon className="w-6 h-6 ml-2" />
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a> </a>
</Link> </Link>
</div> </div>

@ -1533,6 +1533,11 @@
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.0.0.tgz#661b50ebfd25041abb45d8eedd85e7559056bcaf" resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.0.0.tgz#661b50ebfd25041abb45d8eedd85e7559056bcaf"
integrity sha512-mjqRJrgkbcHQBfAHnqH0yRxO/y/22jYrdltpE7WkurafREKZ+pj5bPBwYHMt935Sdz/n16yRcVmsSCqDFHee9A== 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": "@iarna/cli@^1.2.0":
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@iarna/cli/-/cli-1.2.0.tgz#0f7af5e851afe895104583c4ca07377a8094d641" resolved "https://registry.yarnpkg.com/@iarna/cli/-/cli-1.2.0.tgz#0f7af5e851afe895104583c4ca07377a8094d641"

Loading…
Cancel
Save