Merge branch 'develop'

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

@ -458,6 +458,15 @@
"contributions": [
"translation"
]
},
{
"login": "Dabu-dot",
"name": "Dabu-dot",
"avatar_url": "https://avatars.githubusercontent.com/u/52525576?v=4",
"profile": "https://github.com/Dabu-dot",
"contributions": [
"translation"
]
}
],
"badgeTemplate": "<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://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 -->
<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 -->
</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://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>
<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>
<!-- markdownlint-restore -->

@ -178,11 +178,11 @@ If the hostname or IP address you configured above is not accessible outside you
#### Enable Scan
Tick this box if you would like to scan your Radarr/Sonarr server for existing media/request status. It is recommended that you enable this setting, so that users cannot submit requests for media which has already been requested or is already available.
Enable this setting if you would like to scan your Radarr/Sonarr server for existing media/request status. It is recommended that you enable this setting, so that users cannot submit requests for media which has already been requested or is already available.
#### Disable Auto-Search
#### Enable Automatic Search
If you do not want Radarr/Sonarr to automatically search for media upon submission of a request, you can disable this setting.
Enable this setting to have Radarr/Sonarr to automatically search for media upon approval of a request.
## Notifications

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

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

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

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

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

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

@ -175,6 +175,36 @@ requestRoutes.post(
});
}
if (req.body.is4k) {
if (
req.body.mediaType === MediaType.MOVIE &&
!req.user?.hasPermission(
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
{
type: 'or',
}
)
) {
return next({
status: 403,
message: 'You do not have permission to make 4K movie requests.',
});
} else if (
req.body.mediaType === MediaType.TV &&
!req.user?.hasPermission(
[Permission.REQUEST_4K, Permission.REQUEST_4K_TV],
{
type: 'or',
}
)
) {
return next({
status: 403,
message: 'You do not have permission to make 4K series requests.',
});
}
}
const quotas = await requestUser.getQuota();
if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
@ -463,7 +493,6 @@ requestRoutes.get('/:requestId', async (req, res, next) => {
requestRoutes.put<{ requestId: string }>(
'/:requestId',
isAuthenticated(Permission.MANAGE_REQUESTS),
async (req, res, next) => {
const requestRepository = getRepository(MediaRequest);
const userRepository = getRepository(User);
@ -473,17 +502,30 @@ requestRoutes.put<{ requestId: string }>(
);
if (!request) {
return next({ status: 404, message: 'Request not found' });
return next({ status: 404, message: 'Request not found.' });
}
if (
(request.requestedBy.id !== req.user?.id ||
(req.body.mediaType !== 'tv' &&
!req.user?.hasPermission(Permission.REQUEST_ADVANCED))) &&
!req.user?.hasPermission(Permission.MANAGE_REQUESTS)
) {
return next({
status: 403,
message: 'You do not have permission to modify this request.',
});
}
let requestUser = req.user;
if (
req.body.userId &&
!(
req.user?.hasPermission(Permission.MANAGE_USERS) &&
req.user?.hasPermission(Permission.MANAGE_REQUESTS)
)
req.body.userId !== req.user?.id &&
!req.user?.hasPermission([
Permission.MANAGE_USERS,
Permission.MANAGE_REQUESTS,
])
) {
return next({
status: 403,
@ -516,7 +558,7 @@ requestRoutes.put<{ requestId: string }>(
if (!requestedSeasons || requestedSeasons.length === 0) {
throw new Error(
'Missing seasons. If you want to cancel a tv request, use the DELETE method.'
'Missing seasons. If you want to cancel a series request, use the DELETE method.'
);
}
@ -603,7 +645,7 @@ requestRoutes.delete('/:requestId', async (req, res, next) => {
) {
return next({
status: 401,
message: 'You do not have permission to remove this request',
message: 'You do not have permission to delete this request.',
});
}
@ -612,7 +654,7 @@ requestRoutes.delete('/:requestId', async (req, res, next) => {
return res.status(204).send();
} catch (e) {
logger.error(e.message);
next({ status: 404, message: 'Request not found' });
next({ status: 404, message: 'Request not found.' });
}
});
@ -638,7 +680,7 @@ requestRoutes.post<{
label: 'Media Request',
message: e.message,
});
next({ status: 404, message: 'Request not found' });
next({ status: 404, message: 'Request not found.' });
}
}
);
@ -682,7 +724,7 @@ requestRoutes.post<{
label: 'Media Request',
message: e.message,
});
next({ status: 404, message: 'Request not found' });
next({ status: 404, message: 'Request not found.' });
}
}
);

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

@ -81,10 +81,25 @@ router.post(
const body = req.body;
const userRepository = getRepository(User);
const existingUser = await userRepository.findOne({
where: { email: body.email },
});
if (existingUser) {
return next({
status: 409,
message: 'User already exists with submitted email.',
errors: ['USER_EXISTS'],
});
}
const passedExplicitPassword = body.password && body.password.length > 0;
const avatar = gravatarUrl(body.email, { default: 'mm', size: 200 });
if (!passedExplicitPassword && !settings.notifications.agents.email) {
if (
!passedExplicitPassword &&
!settings.notifications.agents.email.enabled
) {
throw new Error('Email notifications must be enabled');
}

@ -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 { uniq } from 'lodash';
import Link from 'next/link';
@ -248,22 +249,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
title={intl.formatMessage(
is4k ? messages.requestcollection4k : messages.requestcollection
)}
iconSvg={
<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>
}
iconSvg={<DuplicateIcon className="w-6 h-6" />}
>
<p>
{intl.formatMessage(
@ -355,20 +341,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
}}
text={
<>
<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>
<DownloadIcon className="w-5 h-5 mr-1" />
<span>
{intl.formatMessage(
hasRequestable
@ -393,20 +366,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
setIs4k(true);
}}
>
<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>
<DownloadIcon className="w-5 h-5 mr-1" />
<span>
{intl.formatMessage(messages.requestcollection4k)}
</span>

@ -1,3 +1,8 @@
import {
ExclamationIcon,
InformationCircleIcon,
XCircleIcon,
} from '@heroicons/react/solid';
import React from 'react';
interface AlertProps {
@ -10,21 +15,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
bgColor: 'bg-yellow-600',
titleColor: 'text-yellow-200',
textColor: 'text-yellow-300',
svg: (
<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>
),
svg: <ExclamationIcon className="w-5 h-5" />,
};
switch (type) {
@ -33,22 +24,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
bgColor: 'bg-indigo-600',
titleColor: 'text-indigo-200',
textColor: 'text-indigo-300',
svg: (
<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>
),
svg: <InformationCircleIcon className="w-5 h-5" />,
};
break;
case 'error':
@ -56,22 +32,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
bgColor: 'bg-red-600',
titleColor: 'text-red-200',
textColor: 'text-red-300',
svg: (
<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>
),
svg: <XCircleIcon className="w-5 h-5" />,
};
break;
}

@ -1,13 +1,14 @@
import { ChevronDownIcon } from '@heroicons/react/solid';
import React, {
useState,
useRef,
AnchorHTMLAttributes,
ReactNode,
ButtonHTMLAttributes,
ReactNode,
useRef,
useState,
} from 'react';
import useClickOutside from '../../../hooks/useClickOutside';
import Transition from '../../Transition';
import { withProperties } from '../../../utils/typeHelpers';
import Transition from '../../Transition';
interface DropdownItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
buttonType?: 'primary' | 'ghost';
@ -102,18 +103,7 @@ const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
{dropdownIcon ? (
dropdownIcon
) : (
<svg
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>
<ChevronDownIcon className="w-5 h-5" />
)}
</button>
<Transition

@ -1,4 +1,4 @@
import React from 'react';
import React, { ReactNode } from 'react';
import ButtonWithDropdown from '../ButtonWithDropdown';
interface PlayButtonProps {
@ -8,6 +8,7 @@ interface PlayButtonProps {
export interface PlayButtonLink {
text: string;
url: string;
svg: ReactNode;
}
const PlayButton: React.FC<PlayButtonProps> = ({ links }) => {
@ -20,27 +21,8 @@ const PlayButton: React.FC<PlayButtonProps> = ({ links }) => {
buttonType="ghost"
text={
<>
<svg
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="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>
{links[0].svg}
{links[0].text}
</>
}
onClick={() => {
@ -57,6 +39,7 @@ const PlayButton: React.FC<PlayButtonProps> = ({ links }) => {
}}
buttonType="ghost"
>
{link.svg}
{link.text}
</ButtonWithDropdown.Item>
);

@ -1,4 +1,5 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { XIcon } from '@heroicons/react/outline';
import React, { useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { useLockBodyScroll } from '../../../hooks/useLockBodyScroll';
@ -81,20 +82,7 @@ const SlideOver: React.FC<SlideOverProps> = ({
className="text-indigo-200 transition duration-150 ease-in-out hover:text-white"
onClick={() => onClose()}
>
<svg
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>
<XIcon className="w-6 h-6" />
</button>
</div>
</div>

@ -1,12 +1,13 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React, { useContext } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import GenreCard from '../../GenreCard';
import Slider from '../../Slider';
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
import { LanguageContext } from '../../../context/LanguageContext';
import GenreCard from '../../GenreCard';
import Slider from '../../Slider';
import { genreColorMap } from '../constants';
import Link from 'next/link';
const messages = defineMessages({
moviegenres: 'Movie Genres',
@ -29,20 +30,7 @@ const MovieGenreSlider: React.FC = () => {
<Link href="/discover/movies/genres">
<a className="slider-title">
<span>{intl.formatMessage(messages.moviegenres)}</span>
<svg
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>
<ArrowCircleRightIcon className="w-6 h-6 ml-2" />
</a>
</Link>
</div>

@ -1,12 +1,13 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React, { useContext } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import GenreCard from '../../GenreCard';
import Slider from '../../Slider';
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
import { LanguageContext } from '../../../context/LanguageContext';
import GenreCard from '../../GenreCard';
import Slider from '../../Slider';
import { genreColorMap } from '../constants';
import Link from 'next/link';
const messages = defineMessages({
tvgenres: 'Series Genres',
@ -29,20 +30,7 @@ const TvGenreSlider: React.FC = () => {
<Link href="/discover/tv/genres">
<a className="slider-title">
<span>{intl.formatMessage(messages.tvgenres)}</span>
<svg
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>
<ArrowCircleRightIcon className="w-6 h-6 ml-2" />
</a>
</Link>
</div>

@ -1,3 +1,4 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -66,20 +67,7 @@ const Discover: React.FC = () => {
<Link href="/requests?filter=all">
<a className="slider-title">
<span>{intl.formatMessage(messages.recentrequests)}</span>
<svg
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>
<ArrowCircleRightIcon className="w-6 h-6 ml-2" />
</a>
</Link>
</div>

@ -1,3 +1,4 @@
import { TranslateIcon } from '@heroicons/react/solid';
import React, { useContext, useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import {
@ -100,18 +101,7 @@ const LanguagePicker: React.FC = () => {
aria-label="Language Picker"
onClick={() => setDropdownOpen(true)}
>
<svg
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>
<TranslateIcon className="w-6 h-6" />
</button>
</div>
<Transition

@ -1,3 +1,4 @@
import { BellIcon } from '@heroicons/react/outline';
import React from 'react';
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"
aria-label="Notifications"
>
<svg
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>
<BellIcon className="w-6 h-6" />
</button>
);
};

@ -1,7 +1,8 @@
import { XCircleIcon } from '@heroicons/react/outline';
import { SearchIcon } from '@heroicons/react/solid';
import React from 'react';
import useSearchInput from '../../../hooks/useSearchInput';
import { defineMessages, useIntl } from 'react-intl';
import ClearButton from '../../../assets/xcircle.svg';
import useSearchInput from '../../../hooks/useSearchInput';
const messages = defineMessages({
searchPlaceholder: 'Search Movies & TV',
@ -18,13 +19,7 @@ const SearchInput: React.FC = () => {
</label>
<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">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<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>
<SearchIcon className="w-5 h-5" />
</div>
<input
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"
onClick={() => clear()}
>
<ClearButton />
<XCircleIcon className="w-5 h-5" />
</button>
)}
</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 { useRouter } from 'next/router';
import React, { ReactNode, useRef } from 'react';
@ -33,20 +40,7 @@ const SidebarLinks: SidebarLinkProps[] = [
href: '/',
messagesKey: 'dashboard',
svgIcon: (
<svg
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>
<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" />
),
activeRegExp: /^\/(discover\/?(movies|tv)?)?$/,
},
@ -54,20 +48,7 @@ const SidebarLinks: SidebarLinkProps[] = [
href: '/requests',
messagesKey: 'requests',
svgIcon: (
<svg
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>
<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" />
),
activeRegExp: /^\/requests/,
},
@ -75,14 +56,7 @@ const SidebarLinks: SidebarLinkProps[] = [
href: '/users',
messagesKey: 'users',
svgIcon: (
<svg
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>
<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" />
),
activeRegExp: /^\/users/,
requiredPermission: Permission.MANAGE_USERS,
@ -91,26 +65,7 @@ const SidebarLinks: SidebarLinkProps[] = [
href: '/settings',
messagesKey: 'settings',
svgIcon: (
<svg
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>
<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" />
),
activeRegExp: /^\/settings/,
requiredPermission: Permission.MANAGE_SETTINGS,
@ -157,19 +112,7 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
aria-label="Close sidebar"
onClick={() => setClosed()}
>
<svg
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>
<XIcon className="w-6 h-6 text-white" />
</button>
</div>
<div

@ -1,10 +1,12 @@
import React, { useState, useRef } from 'react';
import Transition from '../../Transition';
import { useUser } from '../../../hooks/useUser';
import { LogoutIcon } from '@heroicons/react/outline';
import { CogIcon, UserIcon } from '@heroicons/react/solid';
import axios from 'axios';
import useClickOutside from '../../../hooks/useClickOutside';
import { defineMessages, useIntl } from 'react-intl';
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({
myprofile: 'Profile',
@ -65,7 +67,7 @@ const UserDropdown: React.FC = () => {
>
<Link href={`/profile`}>
<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"
tabIndex={0}
onKeyDown={(e) => {
@ -75,12 +77,13 @@ const UserDropdown: React.FC = () => {
}}
onClick={() => setDropdownOpen(false)}
>
{intl.formatMessage(messages.myprofile)}
<UserIcon className="inline w-5 h-5 mr-2" />
<span>{intl.formatMessage(messages.myprofile)}</span>
</a>
</Link>
<Link href={`/profile/settings`}>
<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"
tabIndex={0}
onKeyDown={(e) => {
@ -90,16 +93,18 @@ const UserDropdown: React.FC = () => {
}}
onClick={() => setDropdownOpen(false)}
>
{intl.formatMessage(messages.settings)}
<CogIcon className="inline w-5 h-5 mr-2" />
<span>{intl.formatMessage(messages.settings)}</span>
</a>
</Link>
<a
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"
onClick={() => logout()}
>
{intl.formatMessage(messages.signout)}
<LogoutIcon className="inline w-5 h-5 mr-2" />
<span>{intl.formatMessage(messages.signout)}</span>
</a>
</div>
</div>

@ -1,3 +1,9 @@
import {
ArrowCircleUpIcon,
BeakerIcon,
CodeIcon,
ServerIcon,
} from '@heroicons/react/outline';
import Link from 'next/link';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -51,50 +57,11 @@ const VersionStatus: React.FC<VersionStatusProps> = ({ onClick }) => {
}`}
>
{data.commitTag === 'local' ? (
<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="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
/>
</svg>
<CodeIcon className="w-6 h-6" />
) : data.version.startsWith('develop-') ? (
<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="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>
<BeakerIcon 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 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>
<ServerIcon className="w-6 h-6" />
)}
<div className="flex flex-col flex-1 min-w-0 px-2 truncate last:pr-0">
<span className="font-bold">{versionStream}</span>
@ -114,22 +81,7 @@ const VersionStatus: React.FC<VersionStatusProps> = ({ onClick }) => {
)}
</span>
</div>
{data.updateAvailable && (
<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>
)}
{data.updateAvailable && <ArrowCircleUpIcon className="w-6 h-6" />}
</a>
</Link>
);

@ -1,3 +1,5 @@
import { MenuAlt2Icon } from '@heroicons/react/outline';
import { InformationCircleIcon } from '@heroicons/react/solid';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -57,19 +59,7 @@ const Layout: React.FC = ({ children }) => {
aria-label="Open sidebar"
onClick={() => setSidebarOpen(true)}
>
<svg
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>
<MenuAlt2Icon className="w-6 h-6" />
</button>
<div className="flex justify-between flex-1 pr-4 md:pr-4 md:pl-4">
<SearchInput />
@ -87,18 +77,7 @@ const Layout: React.FC = ({ children }) => {
<div className="p-4 mt-6 bg-indigo-700 rounded-md">
<div className="flex">
<div className="flex-shrink-0">
<svg
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>
<InformationCircleIcon className="w-5 h-5 text-white" />
</div>
<div className="flex-1 ml-3 md:flex md:justify-between">
<p className="text-sm leading-5 text-white">

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

@ -1,3 +1,4 @@
import { XCircleIcon } from '@heroicons/react/solid';
import axios from 'axios';
import { useRouter } from 'next/dist/client/router';
import React, { useEffect, useState } from 'react';
@ -100,19 +101,7 @@ const Login: React.FC = () => {
<div className="p-4 mb-4 bg-red-600 rounded-md">
<div className="flex">
<div className="flex-shrink-0">
<svg
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>
<XCircleIcon className="w-5 h-5 text-red-300" />
</div>
<div className="ml-3">
<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 React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -79,18 +80,7 @@ const ShowMoreCard: React.FC<ShowMoreCardProps> = ({ url, posters }) => {
)}
</div>
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center text-white">
<svg
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>
<ArrowCircleRightIcon className="w-14" />
<div className="mt-2 font-extrabold">
{intl.formatMessage(messages.seemore)}
</div>

@ -1,3 +1,4 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React, { useContext, useEffect } from 'react';
import { useSWRInfinite } from 'swr';
@ -140,20 +141,7 @@ const MediaSlider: React.FC<MediaSliderProps> = ({
<Link href={linkUrl}>
<a className="slider-title">
<span>{title}</span>
<svg
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>
<ArrowCircleRightIcon className="w-6 h-6 ml-2" />
</a>
</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 Link from 'next/link';
import { useRouter } from 'next/router';
@ -50,7 +61,7 @@ const messages = defineMessages({
manageModalTitle: 'Manage Movie',
manageModalRequests: 'Requests',
manageModalNoRequests: 'No requests.',
manageModalClearMedia: 'Clear All Media Data',
manageModalClearMedia: 'Clear Media Data',
manageModalClearMediaWarning:
'* This will irreversibly remove all data for this movie, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.',
studio: '{studioCount, plural, one {Studio} other {Studios}}',
@ -105,6 +116,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
mediaLinks.push({
text: intl.formatMessage(messages.playonplex),
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({
text: intl.formatMessage(messages.play4konplex),
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({
text: intl.formatMessage(messages.watchtrailer),
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"
buttonType="success"
>
<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="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>
<CheckCircleIcon className="w-5 h-5 mr-1" />
<span>{intl.formatMessage(messages.markavailable)}</span>
</Button>
</div>
@ -291,18 +294,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
className="w-full sm:mb-0"
buttonType="success"
>
<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="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>
<CheckCircleIcon className="w-5 h-5 mr-1" />
<span>
{intl.formatMessage(messages.mark4kavailable)}
</span>
@ -341,15 +333,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
className="block mb-2 last:mb-0"
>
<Button buttonType="ghost" 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 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>
<ExternalLinkIcon className="w-5 h-5 mr-1" />
<span>{intl.formatMessage(messages.openradarr)}</span>
</Button>
</a>
@ -361,15 +345,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
rel="noreferrer"
>
<Button buttonType="ghost" 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 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>
<ExternalLinkIcon className="w-5 h-5 mr-1" />
<span>{intl.formatMessage(messages.openradarr4k)}</span>
</Button>
</a>
@ -383,6 +359,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<DocumentRemoveIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.manageModalClearMedia)}
</ConfirmButton>
<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"
onClick={() => setShowManager(true)}
>
<svg
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>
<CogIcon className="w-5" />
</Button>
)}
</div>
@ -513,20 +470,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
<Link href={`/movie/${data.id}/crew`}>
<a className="flex items-center text-gray-400 transition duration-300 hover:text-gray-100">
<span>{intl.formatMessage(messages.viewfullcrew)}</span>
<svg
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>
<ArrowCircleRightIcon className="inline-block w-5 h-5 ml-1" />
</a>
</Link>
</div>
@ -709,20 +653,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
<Link href="/movie/[movieId]/cast" as={`/movie/${data.id}/cast`}>
<a className="slider-title">
<span>{intl.formatMessage(messages.cast)}</span>
<svg
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>
<ArrowCircleRightIcon className="w-6 h-6 ml-2" />
</a>
</Link>
</div>

@ -1,3 +1,4 @@
import { UserCircleIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React, { useState } from 'react';
import CachedImage from '../Common/CachedImage';
@ -57,18 +58,7 @@ const PersonCard: React.FC<PersonCardProps> = ({
/>
</div>
) : (
<svg
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>
<UserCircleIcon className="h-full" />
)}
</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 { defineMessages, useIntl } from 'react-intl';
import globalMessages from '../../i18n/globalMessages';
@ -5,7 +6,7 @@ import PlexOAuth from '../../utils/plex';
const messages = defineMessages({
signinwithplex: 'Sign In',
signingin: 'Signing in…',
signingin: 'Signing In…',
});
const plexOAuth = new PlexOAuth();
@ -48,6 +49,7 @@ const PlexLoginButton: React.FC<PlexLoginButtonProps> = ({
disabled={loading || isProcessing}
className="plex-button"
>
<LoginIcon className="w-5 h-5 mr-1" />
{loading
? intl.formatMessage(globalMessages.loading)
: isProcessing

@ -1,11 +1,12 @@
import React, { useEffect, useMemo, useState } from '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 type { Region } from '../../../server/lib/settings';
import { defineMessages, useIntl } from 'react-intl';
import useSettings from '../../hooks/useSettings';
import { hasFlag } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css';
const messages = defineMessages({
regionDefault: 'All Regions',
@ -125,20 +126,7 @@ const RegionSelector: React.FC<RegionSelectorProps> = ({
: intl.formatMessage(messages.regionDefault)}
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<svg
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>
<ChevronDownIcon className="w-5 h-5 text-gray-500" />
</span>
</Listbox.Button>
</span>
@ -196,18 +184,7 @@ const RegionSelector: React.FC<RegionSelectorProps> = ({
active ? 'text-white' : 'text-indigo-600'
} absolute inset-y-0 left-0 flex items-center pl-1.5`}
>
<svg
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>
<CheckIcon className="w-5 h-5" />
</span>
)}
</div>
@ -234,18 +211,7 @@ const RegionSelector: React.FC<RegionSelectorProps> = ({
active ? 'text-white' : 'text-indigo-600'
} absolute inset-y-0 left-0 flex items-center pl-1.5`}
>
<svg
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>
<CheckIcon className="w-5 h-5" />
</span>
)}
</div>
@ -286,18 +252,7 @@ const RegionSelector: React.FC<RegionSelectorProps> = ({
active ? 'text-white' : 'text-indigo-600'
} absolute inset-y-0 left-0 flex items-center pl-1.5`}
>
<svg
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>
<CheckIcon className="w-5 h-5" />
</span>
)}
</div>

@ -1,3 +1,12 @@
import {
CalendarIcon,
CheckIcon,
EyeIcon,
PencilIcon,
TrashIcon,
UserIcon,
XIcon,
} from '@heroicons/react/solid';
import axios from 'axios';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -69,37 +78,14 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
<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 mb-1 flex-nowrap white">
<svg
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>
<UserIcon className="min-w-0 flex-shrink-0 mr-1.5 h-5 w-5" />
<span className="w-40 truncate md:w-auto">
{request.requestedBy.displayName}
</span>
</div>
{request.modifiedBy && (
<div className="flex flex-nowrap">
<svg
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>
<EyeIcon className="flex-shrink-0 mr-1.5 h-5 w-5" />
<span className="w-40 truncate md:w-auto">
{request.modifiedBy?.displayName}
</span>
@ -115,18 +101,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
onClick={() => updateRequest('approve')}
disabled={isUpdating}
>
<svg
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>
<CheckIcon className="w-4 h-4" />
</Button>
</span>
<span className="mr-1">
@ -135,18 +110,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
onClick={() => updateRequest('decline')}
disabled={isUpdating}
>
<svg
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>
<XIcon className="w-4 h-4" />
</Button>
</span>
<span>
@ -155,14 +119,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
onClick={() => setShowEditModal(true)}
disabled={isUpdating}
>
<svg
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>
<PencilIcon className="w-4 h-4" />
</Button>
</span>
</>
@ -173,18 +130,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
onClick={() => deleteRequest()}
disabled={isUpdating}
>
<svg
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>
<TrashIcon className="w-4 h-4" />
</Button>
)}
</div>
@ -215,18 +161,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
</div>
</div>
<div className="flex items-center mt-2 text-sm leading-5 sm:mt-0">
<svg
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>
<CalendarIcon className="flex-shrink-0 mr-1.5 h-5 w-5" />
<span>
{intl.formatDate(request.createdAt, {
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 React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import {
MediaRequestStatus,
@ -17,19 +23,19 @@ const messages = defineMessages({
viewrequest: 'View Request',
viewrequest4k: 'View 4K Request',
requestmore: 'Request More',
requestmore4k: 'Request More 4K',
requestmore4k: 'Request More in 4K',
approverequest: 'Approve Request',
approverequest4k: 'Approve 4K Request',
declinerequest: 'Decline Request',
declinerequest4k: 'Decline 4K Request',
approverequests:
'Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}',
'Approve {requestCount, plural, one {Request} other {{requestCount} Requests}}',
declinerequests:
'Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}',
'Decline {requestCount, plural, one {Request} other {{requestCount} Requests}}',
approve4krequests:
'Approve {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}',
'Approve {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}',
decline4krequests:
'Decline {requestCount} 4K {requestCount, plural, one {Request} other {Requests}}',
'Decline {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}',
});
interface ButtonOption {
@ -58,26 +64,34 @@ const RequestButton: React.FC<RequestButtonProps> = ({
}) => {
const intl = useIntl();
const settings = useSettings();
const { hasPermission } = useUser();
const { user, hasPermission } = useUser();
const [showRequestModal, setShowRequestModal] = useState(false);
const [showRequest4kModal, setShowRequest4kModal] = useState(false);
const [editRequest, setEditRequest] = useState(false);
const activeRequest = media?.requests.find(
(request) => request.status === MediaRequestStatus.PENDING && !request.is4k
);
const active4kRequest = media?.requests.find(
(request) => request.status === MediaRequestStatus.PENDING && request.is4k
);
// All pending
// All pending requests
const activeRequests = media?.requests.filter(
(request) => request.status === MediaRequestStatus.PENDING && !request.is4k
);
const active4kRequests = media?.requests.filter(
(request) => request.status === MediaRequestStatus.PENDING && request.is4k
);
const activeRequest = useMemo(() => {
return activeRequests && activeRequests.length > 0
? activeRequests.find((request) => request.requestedBy.id === user?.id) ??
activeRequests[0]
: undefined;
}, [activeRequests, user]);
const active4kRequest = useMemo(() => {
return active4kRequests && active4kRequests.length > 0
? active4kRequests.find(
(request) => request.requestedBy.id === user?.id
) ?? active4kRequests[0]
: undefined;
}, [active4kRequests, user]);
const modifyRequest = async (
request: MediaRequest,
type: 'approve' | 'decline'
@ -115,57 +129,10 @@ const RequestButton: React.FC<RequestButtonProps> = ({
id: 'request',
text: intl.formatMessage(globalMessages.request),
action: () => {
setEditRequest(false);
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>
),
});
}
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>
),
svg: <DownloadIcon className="w-5 h-5 mr-1" />,
});
}
@ -181,112 +148,44 @@ const RequestButton: React.FC<RequestButtonProps> = ({
id: 'request4k',
text: intl.formatMessage(globalMessages.request4k),
action: () => {
setEditRequest(false);
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 (
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>
),
svg: <DownloadIcon className="w-5 h-5 mr-1" />,
});
}
if (
activeRequest &&
mediaType === 'movie' &&
hasPermission(Permission.REQUEST)
(activeRequest.requestedBy.id === user?.id ||
(activeRequests?.length === 1 &&
hasPermission(Permission.MANAGE_REQUESTS)))
) {
buttons.push({
id: 'active-request',
text: intl.formatMessage(messages.viewrequest),
action: () => setShowRequestModal(true),
svg: (
<svg
className="w-4 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-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>
),
action: () => {
setEditRequest(true);
setShowRequestModal(true);
},
svg: <InformationCircleIcon className="w-5 h-5 mr-1" />,
});
}
if (
active4kRequest &&
mediaType === 'movie' &&
(hasPermission(Permission.REQUEST_4K) ||
hasPermission(Permission.REQUEST_4K_MOVIE))
(active4kRequest.requestedBy.id === user?.id ||
(active4kRequests?.length === 1 &&
hasPermission(Permission.MANAGE_REQUESTS)))
) {
buttons.push({
id: 'active-4k-request',
text: intl.formatMessage(messages.viewrequest4k),
action: () => setShowRequest4kModal(true),
svg: (
<svg
className="w-4 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-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>
),
action: () => {
setEditRequest(true);
setShowRequest4kModal(true);
},
svg: <InformationCircleIcon className="w-5 h-5 mr-1" />,
});
}
@ -302,20 +201,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
action: () => {
modifyRequest(activeRequest, 'approve');
},
svg: (
<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>
),
svg: <CheckIcon className="w-5 h-5 mr-1" />,
},
{
id: 'decline-request',
@ -323,20 +209,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
action: () => {
modifyRequest(activeRequest, 'decline');
},
svg: (
<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>
),
svg: <XIcon className="w-5 h-5 mr-1" />,
}
);
}
@ -356,20 +229,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
action: () => {
modifyRequests(activeRequests, 'approve');
},
svg: (
<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>
),
svg: <CheckIcon className="w-5 h-5 mr-1" />,
},
{
id: 'decline-request-batch',
@ -379,20 +239,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
action: () => {
modifyRequests(activeRequests, 'decline');
},
svg: (
<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>
),
svg: <XIcon className="w-5 h-5 mr-1" />,
}
);
}
@ -409,20 +256,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
action: () => {
modifyRequest(active4kRequest, 'approve');
},
svg: (
<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>
),
svg: <CheckIcon className="w-5 h-5 mr-1" />,
},
{
id: 'decline-4k-request',
@ -430,20 +264,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
action: () => {
modifyRequest(active4kRequest, 'decline');
},
svg: (
<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>
),
svg: <XIcon className="w-5 h-5 mr-1" />,
}
);
}
@ -456,54 +277,71 @@ const RequestButton: React.FC<RequestButtonProps> = ({
) {
buttons.push(
{
id: 'approve-request-batch',
id: 'approve-4k-request-batch',
text: intl.formatMessage(messages.approve4krequests, {
requestCount: active4kRequests.length,
}),
action: () => {
modifyRequests(active4kRequests, 'approve');
},
svg: (
<svg
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>
),
svg: <CheckIcon className="w-5 h-5 mr-1" />,
},
{
id: 'decline-request-batch',
id: 'decline-4k-request-batch',
text: intl.formatMessage(messages.decline4krequests, {
requestCount: active4kRequests.length,
}),
action: () => {
modifyRequests(active4kRequests, 'decline');
},
svg: (
<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>
),
svg: <XIcon className="w-5 h-5 mr-1" />,
}
);
}
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;
if (!buttonOne) {
@ -516,6 +354,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
tmdbId={tmdbId}
show={showRequestModal}
type={mediaType}
editRequest={editRequest ? activeRequest : undefined}
onComplete={() => {
onUpdate();
setShowRequestModal(false);
@ -526,6 +365,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
tmdbId={tmdbId}
show={showRequest4kModal}
type={mediaType}
editRequest={editRequest ? active4kRequest : undefined}
is4k
onComplete={() => {
onUpdate();

@ -1,3 +1,4 @@
import { CheckIcon, TrashIcon, XIcon } from '@heroicons/react/solid';
import axios from 'axios';
import Link from 'next/link';
import React, { useContext, useEffect } from 'react';
@ -68,20 +69,7 @@ const RequestCardError: React.FC<RequestCardErrorProps> = ({ mediaId }) => {
buttonSize="sm"
onClick={() => deleteRequest()}
>
<svg
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>
<TrashIcon className="w-5 h-5 mr-1" />
<span>{intl.formatMessage(messages.deleterequest)}</span>
</Button>
</div>
@ -261,18 +249,7 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
buttonSize="sm"
onClick={() => modifyRequest('approve')}
>
<svg
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>
<CheckIcon className="w-4 h-4 mr-0 sm:mr-1" />
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.approve)}
</span>
@ -284,18 +261,7 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
buttonSize="sm"
onClick={() => modifyRequest('decline')}
>
<svg
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>
<XIcon className="w-4 h-4 mr-0 sm:mr-1" />
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.decline)}
</span>

@ -1,3 +1,10 @@
import {
CheckIcon,
PencilIcon,
RefreshIcon,
TrashIcon,
XIcon,
} from '@heroicons/react/solid';
import axios from 'axios';
import Link from 'next/link';
import React, { useContext, useState } from 'react';
@ -29,6 +36,7 @@ const messages = defineMessages({
modified: 'Modified',
modifieduserdate: '{date} by {user}',
mediaerror: 'The associated title for this request is no longer available.',
editrequest: 'Edit Request',
deleterequest: 'Delete Request',
cancelRequest: 'Cancel Request',
});
@ -66,20 +74,7 @@ const RequestItemError: React.FC<RequestItemErroProps> = ({
buttonSize="sm"
onClick={() => deleteRequest()}
>
<svg
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>
<TrashIcon className="w-5 h-5 mr-1" />
<span>{intl.formatMessage(messages.deleterequest)}</span>
</Button>
</div>
@ -369,31 +364,6 @@ const RequestItem: React.FC<RequestItemProps> = ({
</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">
{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'] ===
MediaStatus.UNKNOWN &&
requestData.status !== MediaRequestStatus.DECLINED &&
@ -404,19 +374,14 @@ const RequestItem: React.FC<RequestItemProps> = ({
disabled={isRetrying}
onClick={() => retryRequest()}
>
<svg
className="w-5 h-5 mr-1"
fill="currentColor"
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>
<RefreshIcon
className={`w-5 h-5 mr-1 ${isRetrying ? 'animate-spin' : ''}`}
style={{ animationDirection: 'reverse' }}
/>
<span className="block">
{intl.formatMessage(globalMessages.retry)}
{intl.formatMessage(
isRetrying ? globalMessages.retrying : globalMessages.retry
)}
</span>
</Button>
)}
@ -427,94 +392,72 @@ const RequestItem: React.FC<RequestItemProps> = ({
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="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>
<TrashIcon className="w-5 h-5 mr-1" />
<span className="block">
{intl.formatMessage(globalMessages.delete)}
{intl.formatMessage(messages.deleterequest)}
</span>
</ConfirmButton>
)}
{requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<>
<div className="flex flex-row w-full space-x-2">
<span className="w-full">
<Button
className="w-full"
buttonType="success"
onClick={() => modifyRequest('approve')}
>
<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="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>
<div className="flex flex-row w-full space-x-2">
<span className="w-full">
<Button
className="w-full"
buttonType="success"
onClick={() => modifyRequest('approve')}
>
<CheckIcon className="w-5 h-5 mr-1" />
<span className="block">
{intl.formatMessage(globalMessages.approve)}
</span>
</Button>
</span>
<span className="w-full">
<Button
className="w-full"
buttonType="primary"
onClick={() => setShowEditModal(true)}
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 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>
<XIcon className="w-5 h-5 mr-1" />
<span className="block">
{intl.formatMessage(globalMessages.edit)}
{intl.formatMessage(globalMessages.decline)}
</span>
</Button>
</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>

@ -1,3 +1,4 @@
import { FilterIcon, SortDescendingIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
@ -119,18 +120,7 @@ const RequestList: React.FC = () => {
<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">
<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
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>
<FilterIcon className="w-6 h-6" />
</span>
<select
id="filter"
@ -169,14 +159,7 @@ const RequestList: React.FC = () => {
</div>
<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">
<svg
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>
<SortDescendingIcon className="w-6 h-6" />
</span>
<select
id="sort"

@ -1,5 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { Listbox, Transition } from '@headlessui/react';
import {
AdjustmentsIcon,
CheckIcon,
ChevronDownIcon,
} from '@heroicons/react/solid';
import { isEqual } from 'lodash';
import dynamic from 'next/dynamic';
import React, { useEffect, useState } from 'react';
@ -269,15 +274,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
return (
<>
<div className="flex items-center mb-2 font-bold tracking-wider">
<svg
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>
<AdjustmentsIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.advancedoptions)}
</div>
<div className="p-4 bg-gray-600 rounded-md shadow">
@ -526,19 +523,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
</span>
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<svg
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>
<ChevronDownIcon className="w-5 h-5 text-gray-500" />
</span>
</Listbox.Button>
</span>
@ -592,18 +577,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
: 'text-indigo-600'
} absolute inset-y-0 left-0 flex items-center pl-1.5`}
>
<svg
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>
<CheckIcon className="w-5 h-5" />
</span>
)}
</div>

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

@ -1,3 +1,4 @@
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -90,31 +91,9 @@ const QuotaDisplay: React.FC<QuotaDisplayProps> = ({
</div>
<div className="flex justify-end flex-1">
{showDetails ? (
<svg
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>
<ChevronUpIcon className="w-6 h-6" />
) : (
<svg
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>
<ChevronDownIcon className="w-6 h-6" />
)}
</div>
</div>

@ -1,3 +1,4 @@
import { DownloadIcon } from '@heroicons/react/outline';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
@ -51,22 +52,7 @@ const SearchByNameModal: React.FC<SearchByNameModalProps> = ({
okText={intl.formatMessage(globalMessages.next)}
okDisabled={!tvdbId}
okButtonType="primary"
iconSvg={
<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>
}
iconSvg={<DownloadIcon className="w-6 h-6" />}
>
<Alert
title={intl.formatMessage(messages.notvdbiddescription)}

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

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

@ -1,7 +1,8 @@
import { ClipboardCopyIcon } from '@heroicons/react/solid';
import React, { useEffect } from 'react';
import useClipboard from 'react-use-clipboard';
import { useToasts } from 'react-toast-notifications';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useClipboard from 'react-use-clipboard';
const messages = defineMessages({
copied: 'Copied API key to clipboard.',
@ -29,17 +30,9 @@ const CopyButton: React.FC<{ textToCopy: string }> = ({ textToCopy }) => {
e.preventDefault();
setCopied();
}}
className="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-500 text-sm leading-5 font-medium text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
className="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium leading-5 text-white transition duration-150 ease-in-out bg-indigo-600 border border-gray-500 hover:bg-indigo-500 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700"
>
<svg
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>
<ClipboardCopyIcon className="w-5 h-5 text-white" />
</button>
);
};

@ -1,3 +1,4 @@
import { CheckIcon, XIcon } from '@heroicons/react/solid';
import React from 'react';
interface LibraryItemProps {
@ -12,8 +13,8 @@ const LibraryItem: React.FC<LibraryItemProps> = ({
onToggle,
}) => {
return (
<li className="col-span-1 flex shadow-sm rounded-md">
<div className="flex-1 flex items-center justify-between border-t border-r border-b border-gray-700 bg-gray-600 rounded-md truncate">
<li className="flex col-span-1 rounded-md shadow-sm">
<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">
{name}
</div>
@ -45,19 +46,7 @@ const LibraryItem: React.FC<LibraryItemProps> = ({
: 'opacity-100 ease-in duration-200'
} absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`}
>
<svg
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>
<XIcon className="w-3 h-3 text-gray-400" />
</span>
<span
className={`${
@ -66,13 +55,7 @@ const LibraryItem: React.FC<LibraryItemProps> = ({
: 'opacity-0 ease-out duration-100'
} absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`}
>
<svg
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>
<CheckIcon className="w-3 h-3 text-indigo-600" />
</span>
</span>
</span>

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

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

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

@ -1,3 +1,4 @@
import { QuestionMarkCircleIcon, RefreshIcon } from '@heroicons/react/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import dynamic from 'next/dynamic';
@ -232,18 +233,7 @@ const NotificationsWebhook: React.FC = () => {
}}
className="mr-2"
>
<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 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>
<RefreshIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.resetPayload)}
</Button>
<a
@ -252,18 +242,7 @@ const NotificationsWebhook: React.FC = () => {
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"
>
<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="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>
<QuestionMarkCircleIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.templatevariablehelp)}
</a>
</div>

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

@ -1,3 +1,4 @@
import { DocumentTextIcon } from '@heroicons/react/outline';
import React, { useState } from 'react';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
import ReactMarkdown from 'react-markdown';
@ -70,22 +71,7 @@ const Release: React.FC<ReleaseProps> = ({
>
<Modal
onCancel={() => setModalOpen(false)}
iconSvg={
<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>
}
iconSvg={<DocumentTextIcon className="w-6 h-6" />}
title={intl.formatMessage(messages.versionChangelog)}
cancelText={intl.formatMessage(globalMessages.close)}
okText={intl.formatMessage(messages.viewongithub)}
@ -126,6 +112,7 @@ const Release: React.FC<ReleaseProps> = ({
</div>
<div className="flex-1 text-center sm:text-right">
<Button buttonType="primary" onClick={() => setModalOpen(true)}>
<DocumentTextIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.viewchangelog)}
</Button>
</div>

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

@ -1,3 +1,10 @@
import {
ClipboardCopyIcon,
DocumentSearchIcon,
FilterIcon,
PauseIcon,
PlayIcon,
} from '@heroicons/react/solid';
import copy from 'copy-to-clipboard';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
@ -135,6 +142,7 @@ const SettingsLogs: React.FC = () => {
>
<Modal
title={intl.formatMessage(messages.logDetails)}
iconSvg={<DocumentSearchIcon className="w-6 h-6" />}
onCancel={() => setActiveLog(null)}
cancelText={intl.formatMessage(globalMessages.close)}
onOk={() => (activeLog ? copyLogString(activeLog) : undefined)}
@ -237,31 +245,9 @@ const SettingsLogs: React.FC = () => {
>
<span>
{refreshInterval ? (
<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="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>
<PauseIcon className="w-5 h-5 mr-1" />
) : (
<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="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>
<PlayIcon className="w-5 h-5 mr-1" />
)}
</span>
<span>
@ -273,18 +259,7 @@ const SettingsLogs: React.FC = () => {
</div>
<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">
<svg
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>
<FilterIcon className="w-6 h-6" />
</span>
<select
id="filter"
@ -360,19 +335,7 @@ const SettingsLogs: React.FC = () => {
onClick={() => setActiveLog(row)}
className="mr-2"
>
<svg
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>
<DocumentSearchIcon className="w-5 h-5 text-white" />
</Button>
)}
<Button
@ -380,15 +343,7 @@ const SettingsLogs: React.FC = () => {
buttonSize="sm"
onClick={() => copyLogString(row)}
>
<svg
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>
<ClipboardCopyIcon className="w-5 h-5 text-white" />
</Button>
</Table.TD>
</tr>

@ -1,3 +1,4 @@
import { RefreshIcon } from '@heroicons/react/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
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"
>
<svg
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>
<RefreshIcon className="w-5 h-5" />
</button>
</div>
</div>

@ -1,6 +1,7 @@
import { AtSymbolIcon } from '@heroicons/react/outline';
import { LightningBoltIcon } from '@heroicons/react/solid';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import Bolt from '../../assets/bolt.svg';
import DiscordLogo from '../../assets/extlogos/discord.svg';
import PushbulletLogo from '../../assets/extlogos/pushbullet.svg';
import PushoverLogo from '../../assets/extlogos/pushover.svg';
@ -27,20 +28,7 @@ const SettingsNotifications: React.FC = ({ children }) => {
text: intl.formatMessage(messages.email),
content: (
<span className="flex items-center">
<svg
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>
<AtSymbolIcon className="h-4 mr-2" />
{intl.formatMessage(messages.email)}
</span>
),
@ -106,7 +94,7 @@ const SettingsNotifications: React.FC = ({ children }) => {
text: intl.formatMessage(messages.webhook),
content: (
<span className="flex items-center">
<Bolt className="h-4 mr-2" />
<LightningBoltIcon className="h-4 mr-2" />
{intl.formatMessage(messages.webhook)}
</span>
),

@ -1,5 +1,7 @@
import { RefreshIcon, SearchIcon, XIcon } from '@heroicons/react/solid';
import axios from 'axios';
import { Field, Formik } from 'formik';
import { orderBy } from 'lodash';
import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@ -7,7 +9,6 @@ import useSWR from 'swr';
import * as Yup from 'yup';
import type { PlexDevice } from '../../../server/interfaces/api/plexInterfaces';
import type { PlexSettings } from '../../../server/lib/settings';
import Spinner from '../../assets/spinner.svg';
import globalMessages from '../../i18n/globalMessages';
import Alert from '../Common/Alert';
import Badge from '../Common/Badge';
@ -28,7 +29,7 @@ const messages = defineMessages({
serverpresetPlaceholder: 'Plex Server',
serverLocal: 'local',
serverRemote: 'remote',
serverConnected: 'connected',
serverSecure: 'secure',
serverpresetManualMessage: 'Manual configuration',
serverpresetRefreshing: 'Retrieving servers…',
serverpresetLoad: 'Press the button to load available servers',
@ -131,7 +132,7 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
dev.connection.forEach((conn) =>
finalPresets.push({
name: dev.name,
ssl: !conn.local && conn.protocol === 'https',
ssl: conn.protocol === 'https',
uri: conn.uri,
address: conn.address,
port: conn.port,
@ -141,14 +142,8 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
})
);
});
finalPresets.sort((a, b) => {
if (a.status && !b.status) {
return -1;
} else {
return 1;
}
});
return finalPresets;
return orderBy(finalPresets, ['status', 'ssl'], ['desc', 'desc']);
}, [availableServers]);
const syncLibraries = async () => {
@ -420,7 +415,13 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
server.local
? intl.formatMessage(messages.serverLocal)
: intl.formatMessage(messages.serverRemote)
}]
}]${
server.ssl
? ` [${intl.formatMessage(
messages.serverSecure
)}]`
: ''
}
${server.status ? '' : '(' + server.message + ')'}
`}
</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"
>
{isRefreshingPresets ? (
<Spinner className="w-5 h-5" />
) : (
<svg
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>
)}
<RefreshIcon
className={`w-5 h-5 ${
isRefreshingPresets ? 'animate-spin' : ''
}`}
style={{ animationDirection: 'reverse' }}
/>
</button>
</div>
</div>
@ -538,18 +529,10 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
</div>
<div className="section">
<Button onClick={() => syncLibraries()} disabled={isSyncing}>
<svg
className={`${isSyncing ? 'animate-spin' : ''} 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>
<RefreshIcon
className={`w-5 h-5 mr-1 ${isSyncing ? 'animate-spin' : ''}`}
style={{ animationDirection: 'reverse' }}
/>
{isSyncing
? intl.formatMessage(messages.scanning)
: intl.formatMessage(messages.scan)}
@ -623,40 +606,14 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
<div className="flex-1 text-right">
{!dataSync?.running && (
<Button buttonType="warning" onClick={() => startScan()}>
<svg
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>
<SearchIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.startscan)}
</Button>
)}
{dataSync?.running && (
<Button buttonType="danger" onClick={() => cancelScan()}>
<svg
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>
<XIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.cancelscan)}
</Button>
)}

@ -1,3 +1,4 @@
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/solid';
import axios from 'axios';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -130,17 +131,8 @@ const ServerInstance: React.FC<ServerInstanceProps> = ({
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"
>
<svg
className="w-5 h-5"
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>
<PencilIcon className="w-5 h-5 mr-2" />
{intl.formatMessage(globalMessages.edit)}
</button>
</div>
<div className="flex flex-1 w-0 -ml-px">
@ -148,21 +140,8 @@ const ServerInstance: React.FC<ServerInstanceProps> = ({
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"
>
<svg
className="w-5 h-5"
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>
<TrashIcon className="w-5 h-5 mr-2" />
{intl.formatMessage(globalMessages.delete)}
</button>
</div>
</div>
@ -278,6 +257,7 @@ const SettingsServices: React.FC = () => {
})
}
title="Delete Server"
iconSvg={<TrashIcon className="w-6 h-6" />}
>
{intl.formatMessage(messages.deleteserverconfirm)}
</Modal>
@ -343,18 +323,7 @@ const SettingsServices: React.FC = () => {
setEditRadarrModal({ open: true, radarr: null })
}
>
<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="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>
<PlusIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.addradarr)}
</Button>
</div>
@ -434,18 +403,7 @@ const SettingsServices: React.FC = () => {
setEditSonarrModal({ open: true, sonarr: null })
}
>
<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="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>
<PlusIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.addsonarr)}
</Button>
</div>

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

@ -1,3 +1,4 @@
import { CheckIcon } from '@heroicons/react/solid';
import React from 'react';
interface CurrentStep {
@ -17,26 +18,13 @@ const SetupSteps: React.FC<CurrentStep> = ({
}) => {
return (
<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
className={`flex-shrink-0 w-10 h-10 flex items-center justify-center border-2
${active ? 'border-indigo-600 ' : 'border-white '}
${completed ? 'bg-indigo-600 border-indigo-600 ' : ''} rounded-full`}
>
{completed && (
<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 && <CheckIcon className="w-6 h-6 text-white" />}
{!completed && (
<p className={active ? 'text-white' : 'text-indigo-200'}>
{stepNumber}
@ -53,9 +41,9 @@ const SetupSteps: React.FC<CurrentStep> = ({
</div>
{!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
className="h-full w-full text-gray-600"
className="w-full h-full text-gray-600"
viewBox="0 0 22 80"
fill="none"
preserveAspectRatio="none"

@ -1,3 +1,4 @@
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/outline';
import { debounce } from 'lodash';
import React, {
ReactNode,
@ -166,20 +167,7 @@ const Slider: React.FC<SliderProps> = ({
onClick={() => slide(Direction.LEFT)}
disabled={scrollPos.isStart}
>
<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="M15 19l-7-7 7-7"
/>
</svg>
<ChevronLeftIcon className="w-6 h-6" />
</button>
<button
className={`${
@ -188,20 +176,7 @@ const Slider: React.FC<SliderProps> = ({
onClick={() => slide(Direction.RIGHT)}
disabled={scrollPos.isEnd}
>
<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 5l7 7-7 7"
/>
</svg>
<ChevronRightIcon className="w-6 h-6" />
</button>
</div>
<div

@ -1,3 +1,4 @@
import { SparklesIcon } from '@heroicons/react/outline';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
@ -38,22 +39,7 @@ const StatusChecker: React.FC = () => {
show={data.commitTag !== process.env.commitTag}
>
<Modal
iconSvg={
<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>
}
iconSvg={<SparklesIcon className="w-6 h-6" />}
title={intl.formatMessage(messages.newversionavailable)}
onOk={() => location.reload()}
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 React, { useCallback, useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
@ -128,32 +130,12 @@ const TitleCard: React.FC<TitleCardProps> = ({
{(currentStatus === MediaStatus.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">
<svg
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>
<CheckIcon className="w-3 h-3 sm:w-4 sm:h-4" />
</div>
)}
{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">
<svg
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>
<BellIcon className="w-3 h-3 sm:w-4 sm:h-4" />
</div>
)}
{currentStatus === MediaStatus.PROCESSING && (
@ -161,18 +143,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
{inProgress ? (
<Spinner className="w-3 h-3" />
) : (
<svg
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>
<ClockIcon className="w-3 h-3 sm:w-4 sm:h-4" />
)}
</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"
>
<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>
<DownloadIcon className="w-4 h-4 mr-1" />
<span className="text-xs">
{intl.formatMessage(globalMessages.request)}
</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 type { ToastProps } from 'react-toast-notifications';
const Toast: React.FC<ToastProps> = ({ appearance, children, onDismiss }) => {
return (
<div className="toast flex items-end justify-center px-2 py-2 pointer-events-none 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="rounded-lg ring-1 ring-black ring-opacity-5 overflow-hidden">
<div className="flex items-end justify-center px-2 py-2 pointer-events-none toast sm:items-start sm:justify-end">
<div className="w-full max-w-sm bg-gray-700 rounded-lg shadow-lg pointer-events-auto">
<div className="overflow-hidden rounded-lg ring-1 ring-black ring-opacity-5">
<div className="p-4">
<div className="flex items-start">
<div className="flex-shrink-0">
{appearance === 'success' && (
<svg
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>
<CheckCircleIcon className="w-6 h-6 text-green-400" />
)}
{appearance === 'error' && (
<svg
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>
<ExclamationCircleIcon className="w-6 h-6 text-red-500" />
)}
{appearance === 'info' && (
<svg
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>
<InformationCircleIcon className="w-6 h-6 text-indigo-500" />
)}
{appearance === 'warning' && (
<svg
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>
<ExclamationIcon className="w-6 h-6 text-orange-400" />
)}
</div>
<div className="ml-3 w-0 flex-1 text-white">{children}</div>
<div className="ml-4 flex-shrink-0 flex">
<div className="flex-1 w-0 ml-3 text-white">{children}</div>
<div className="flex flex-shrink-0 ml-4">
<button
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
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>
<XIcon className="w-5 h-5" />
</button>
</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 Link from 'next/link';
import { useRouter } from 'next/router';
@ -50,7 +61,7 @@ const messages = defineMessages({
manageModalTitle: 'Manage Series',
manageModalRequests: 'Requests',
manageModalNoRequests: 'No requests.',
manageModalClearMedia: 'Clear All Media Data',
manageModalClearMedia: 'Clear Media Data',
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.',
originaltitle: 'Original Title',
@ -113,6 +124,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
mediaLinks.push({
text: intl.formatMessage(messages.playonplex),
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({
text: intl.formatMessage(messages.play4konplex),
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({
text: intl.formatMessage(messages.watchtrailer),
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"
buttonType="success"
>
<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="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>
<CheckCircleIcon className="w-5 h-5 mr-1" />
<span>{intl.formatMessage(messages.markavailable)}</span>
</Button>
</div>
@ -327,18 +330,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
className="w-full sm:mb-0"
buttonType="success"
>
<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="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>
<CheckCircleIcon className="w-5 h-5 mr-1" />
<span>
{intl.formatMessage(messages.mark4kavailable)}
</span>
@ -380,15 +372,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
className="block mb-2 last:mb-0"
>
<Button buttonType="ghost" 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 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>
<ExternalLinkIcon className="w-5 h-5 mr-1" />
<span>{intl.formatMessage(messages.opensonarr)}</span>
</Button>
</a>
@ -400,15 +384,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
rel="noreferrer"
>
<Button buttonType="ghost" 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 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>
<ExternalLinkIcon className="w-5 h-5 mr-1" />
<span>{intl.formatMessage(messages.opensonarr4k)}</span>
</Button>
</a>
@ -422,6 +398,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<DocumentRemoveIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.manageModalClearMedia)}
</ConfirmButton>
<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"
onClick={() => setShowManager(true)}
>
<svg
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>
<CogIcon className="w-5" />
</Button>
)}
</div>
@ -564,20 +522,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
<Link href={`/tv/${data.id}/crew`}>
<a className="flex items-center text-gray-400 transition duration-300 hover:text-gray-100">
<span>{intl.formatMessage(messages.viewfullcrew)}</span>
<svg
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>
<ArrowCircleRightIcon className="inline-block w-5 h-5 ml-1" />
</a>
</Link>
</div>
@ -734,20 +679,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
<Link href="/tv/[tvId]/cast" as={`/tv/${data.id}/cast`}>
<a className="slider-title">
<span>{intl.formatMessage(messages.cast)}</span>
<svg
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>
<ArrowCircleRightIcon className="w-6 h-6 ml-2" />
</a>
</Link>
</div>

@ -1,3 +1,4 @@
import { PencilIcon } from '@heroicons/react/solid';
import axios from 'axios';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -84,6 +85,7 @@ const BulkEditModal: React.FC<BulkEditProps> = ({
return (
<Modal
title={intl.formatMessage(messages.edituser)}
iconSvg={<PencilIcon className="w-6 h-6" />}
onOk={() => {
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 { Field, Form, Formik } from 'formik';
import Link from 'next/link';
@ -10,7 +17,6 @@ import * as Yup from 'yup';
import type { UserResultsResponse } from '../../../server/interfaces/api/userInterfaces';
import { UserSettingsNotificationsResponse } from '../../../server/interfaces/api/userSettingsInterfaces';
import { hasPermission } from '../../../server/lib/permissions';
import AddUserIcon from '../../assets/useradd.svg';
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
import { Permission, User, UserType, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
@ -56,6 +62,8 @@ const messages = defineMessages({
validationpasswordminchars:
'Password is too short; should be a minimum of 8 characters',
usercreatedfailed: 'Something went wrong while creating the user.',
usercreatedfailedexisting:
'The provided email address is already in use by another user.',
usercreatedsuccess: 'User created successfully!',
email: 'Email Address',
password: 'Password',
@ -265,22 +273,7 @@ const UserList: React.FC = () => {
okButtonType="danger"
onCancel={() => setDeleteModal({ isOpen: false })}
title={intl.formatMessage(messages.deleteuser)}
iconSvg={
<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>
}
iconSvg={<TrashIcon className="w-6 h-6" />}
>
{intl.formatMessage(messages.deleteconfirm)}
</Modal>
@ -314,10 +307,17 @@ const UserList: React.FC = () => {
});
setCreateModal({ isOpen: false });
} catch (e) {
addToast(intl.formatMessage(messages.usercreatedfailed), {
appearance: 'error',
autoDismiss: true,
});
addToast(
intl.formatMessage(
e.response.data.errors?.includes('USER_EXISTS')
? messages.usercreatedfailedexisting
: messages.usercreatedfailed
),
{
appearance: 'error',
autoDismiss: true,
}
);
} finally {
revalidate();
}
@ -335,7 +335,7 @@ const UserList: React.FC = () => {
return (
<Modal
title={intl.formatMessage(messages.createuser)}
iconSvg={<AddUserIcon className="h-6" />}
iconSvg={<UserAddIcon className="w-6 h-6" />}
onOk={() => handleSubmit()}
okText={
isSubmitting
@ -445,12 +445,13 @@ const UserList: React.FC = () => {
<div className="flex flex-col justify-between lg:items-end lg:flex-row">
<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-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
className="flex-grow mr-2 outline"
className="flex-grow mb-2 sm:mb-0 sm:mr-2 outline"
buttonType="primary"
onClick={() => setCreateModal({ isOpen: true })}
>
<UserAddIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.createlocaluser)}
</Button>
<Button
@ -459,19 +460,13 @@ const UserList: React.FC = () => {
disabled={isImporting}
onClick={() => importFromPlex()}
>
<InboxInIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.importfromplex)}
</Button>
</div>
<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">
<svg
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>
<SortDescendingIcon className="w-6 h-6" />
</span>
<select
id="sort"
@ -528,6 +523,7 @@ const UserList: React.FC = () => {
onClick={() => setShowBulkEditModal(true)}
disabled={selectedUsers.length === 0}
>
<PencilIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.bulkedit)}
</Button>
)}

@ -1,3 +1,4 @@
import { CogIcon, UserIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -92,18 +93,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
passHref
>
<Button as="a">
<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="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>
<CogIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.settings)}
</Button>
</Link>
@ -116,18 +106,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
passHref
>
<Button as="a">
<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="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clipRule="evenodd"
/>
</svg>
<UserIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.profile)}
</Button>
</Link>

@ -1,3 +1,4 @@
import { AtSymbolIcon } from '@heroicons/react/outline';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -33,20 +34,7 @@ const UserNotificationSettings: React.FC = ({ children }) => {
text: intl.formatMessage(messages.email),
content: (
<span className="flex items-center">
<svg
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>
<AtSymbolIcon className="h-4 mr-2" />
{intl.formatMessage(messages.email)}
</span>
),

@ -1,3 +1,4 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useState } from 'react';
@ -236,20 +237,7 @@ const UserProfile: React.FC = () => {
<Link href={`/users/${user?.id}/requests?filter=all`}>
<a className="slider-title">
<span>{intl.formatMessage(messages.recentrequests)}</span>
<svg
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>
<ArrowCircleRightIcon className="w-6 h-6 ml-2" />
</a>
</Link>
</div>

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

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

@ -1,3 +1,4 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -22,22 +23,9 @@ const Custom404: React.FC = () => {
})}
</div>
<Link href="/">
<a className="flex">
<a className="flex mt-2">
{intl.formatMessage(messages.returnHome)}
<svg
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>
<ArrowCircleRightIcon className="w-6 h-6 ml-2" />
</a>
</Link>
</div>

@ -1,9 +1,10 @@
import React from 'react';
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import type { NextPage } from 'next';
import Link from 'next/link';
import type { Undefinable } from '../utils/typeHelpers';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import PageTitle from '../components/Common/PageTitle';
import type { Undefinable } from '../utils/typeHelpers';
interface ErrorProps {
statusCode?: number;
@ -45,22 +46,9 @@ const Error: NextPage<ErrorProps> = ({ statusCode }) => {
: getErrorMessage(statusCode)}
</div>
<Link href="/">
<a className="flex">
<a className="flex mt-2">
{intl.formatMessage(messages.returnHome)}
<svg
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>
<ArrowCircleRightIcon className="w-6 h-6 ml-2" />
</a>
</Link>
</div>

@ -1533,6 +1533,11 @@
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.0.0.tgz#661b50ebfd25041abb45d8eedd85e7559056bcaf"
integrity sha512-mjqRJrgkbcHQBfAHnqH0yRxO/y/22jYrdltpE7WkurafREKZ+pj5bPBwYHMt935Sdz/n16yRcVmsSCqDFHee9A==
"@heroicons/react@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.1.tgz#66d25f6441920bd5c2146ea27fd33995885452dd"
integrity sha512-uikw2gKCmqnvjVxitecWfFLMOKyL9BTFcU4VM3hHj9OMwpkCr5Ke+MRMyY2/aQVmsYs4VTq7NCFX05MYwAHi3g==
"@iarna/cli@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@iarna/cli/-/cli-1.2.0.tgz#0f7af5e851afe895104583c4ca07377a8094d641"

Loading…
Cancel
Save