You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
692 lines
24 KiB
692 lines
24 KiB
import React, { useMemo, useState } from 'react';
|
|
import LoadingSpinner from '../Common/LoadingSpinner';
|
|
import type { PlexSettings } from '../../../server/lib/settings';
|
|
import type { PlexDevice } from '../../../server/interfaces/api/plexInterfaces';
|
|
import useSWR from 'swr';
|
|
import { useToasts } from 'react-toast-notifications';
|
|
import { Formik, Field } from 'formik';
|
|
import Button from '../Common/Button';
|
|
import axios from 'axios';
|
|
import LibraryItem from './LibraryItem';
|
|
import Badge from '../Common/Badge';
|
|
import { defineMessages, useIntl } from 'react-intl';
|
|
import * as Yup from 'yup';
|
|
import Alert from '../Common/Alert';
|
|
import Spinner from '../../assets/spinner.svg';
|
|
import PageTitle from '../Common/PageTitle';
|
|
import globalMessages from '../../i18n/globalMessages';
|
|
|
|
const messages = defineMessages({
|
|
plex: 'Plex',
|
|
plexsettings: 'Plex Settings',
|
|
plexsettingsDescription:
|
|
'Configure the settings for your Plex server. Overseerr scans your Plex libraries to see what content is available.',
|
|
servername: 'Server Name',
|
|
servernameTip: 'Automatically retrieved from Plex after saving',
|
|
servernamePlaceholder: 'Plex Server Name',
|
|
serverpreset: 'Server',
|
|
serverpresetPlaceholder: 'Plex Server',
|
|
serverLocal: 'local',
|
|
serverRemote: 'remote',
|
|
serverConnected: 'connected',
|
|
serverpresetManualMessage: 'Manual configuration',
|
|
serverpresetRefreshing: 'Retrieving servers…',
|
|
serverpresetLoad: 'Press the button to load available servers',
|
|
toastPlexRefresh: 'Retrieving server list from Plex',
|
|
toastPlexRefreshSuccess: 'Plex server list retrieved successfully!',
|
|
toastPlexRefreshFailure: 'Failed to retrieve Plex server list.',
|
|
toastPlexConnecting: 'Attempting to connect to Plex…',
|
|
toastPlexConnectingSuccess: 'Plex connection established successfully!',
|
|
toastPlexConnectingFailure: 'Failed to connect to Plex.',
|
|
settingUpPlex: 'Setting Up Plex',
|
|
settingUpPlexDescription:
|
|
'To set up Plex, you can either enter your details manually \
|
|
or select a server retrieved from <RegisterPlexTVLink>plex.tv</RegisterPlexTVLink>.\
|
|
Press the button to the right of the dropdown to check connectivity and retrieve available servers.',
|
|
hostname: 'Hostname or IP Address',
|
|
port: 'Port',
|
|
ssl: 'SSL',
|
|
timeout: 'Timeout',
|
|
save: 'Save Changes',
|
|
saving: 'Saving…',
|
|
plexlibraries: 'Plex Libraries',
|
|
plexlibrariesDescription:
|
|
'The libraries Overseerr scans for titles. Set up and save your Plex connection settings, then click the button below if no libraries are listed.',
|
|
scanning: 'Scanning…',
|
|
scan: 'Scan Plex Libraries',
|
|
manualscan: 'Manual Library Scan',
|
|
manualscanDescription:
|
|
"Normally, this will only be run once every 24 hours. Overseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!",
|
|
notrunning: 'Not Running',
|
|
currentlibrary: 'Current Library: {name}',
|
|
librariesRemaining: 'Libraries Remaining: {count}',
|
|
startscan: 'Start Scan',
|
|
cancelscan: 'Cancel Scan',
|
|
validationHostnameRequired: 'You must provide a hostname or IP address',
|
|
validationPortRequired: 'You must provide a valid port number',
|
|
});
|
|
|
|
interface Library {
|
|
id: string;
|
|
name: string;
|
|
enabled: boolean;
|
|
}
|
|
|
|
interface SyncStatus {
|
|
running: boolean;
|
|
progress: number;
|
|
total: number;
|
|
currentLibrary?: Library;
|
|
libraries: Library[];
|
|
}
|
|
|
|
interface PresetServerDisplay {
|
|
name: string;
|
|
ssl: boolean;
|
|
uri: string;
|
|
address: string;
|
|
host?: string;
|
|
port: number;
|
|
local: boolean;
|
|
status?: boolean;
|
|
message?: string;
|
|
}
|
|
interface SettingsPlexProps {
|
|
onComplete?: () => void;
|
|
}
|
|
|
|
const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
|
|
const [isSyncing, setIsSyncing] = useState(false);
|
|
const [isRefreshingPresets, setIsRefreshingPresets] = useState(false);
|
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
const [availableServers, setAvailableServers] = useState<PlexDevice[] | null>(
|
|
null
|
|
);
|
|
const {
|
|
data: data,
|
|
error: error,
|
|
revalidate: revalidate,
|
|
} = useSWR<PlexSettings>('/api/v1/settings/plex');
|
|
const { data: dataSync, revalidate: revalidateSync } = useSWR<SyncStatus>(
|
|
'/api/v1/settings/plex/sync',
|
|
{
|
|
refreshInterval: 1000,
|
|
}
|
|
);
|
|
const intl = useIntl();
|
|
const { addToast, removeToast } = useToasts();
|
|
const PlexSettingsSchema = Yup.object().shape({
|
|
hostname: Yup.string()
|
|
.required(intl.formatMessage(messages.validationHostnameRequired))
|
|
.matches(
|
|
// eslint-disable-next-line
|
|
/^(([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])$/i,
|
|
intl.formatMessage(messages.validationHostnameRequired)
|
|
),
|
|
port: Yup.number()
|
|
.typeError(intl.formatMessage(messages.validationPortRequired))
|
|
.required(intl.formatMessage(messages.validationPortRequired)),
|
|
});
|
|
|
|
const activeLibraries =
|
|
data?.libraries
|
|
.filter((library) => library.enabled)
|
|
.map((library) => library.id) ?? [];
|
|
|
|
const availablePresets = useMemo(() => {
|
|
const finalPresets: PresetServerDisplay[] = [];
|
|
availableServers?.forEach((dev) => {
|
|
dev.connection.forEach((conn) =>
|
|
finalPresets.push({
|
|
name: dev.name,
|
|
ssl: conn.protocol === 'https' ? true : false,
|
|
uri: conn.uri,
|
|
address: conn.address,
|
|
port: conn.port,
|
|
local: conn.local,
|
|
host: conn.host,
|
|
status: conn.status === 200 ? true : false,
|
|
message: conn.message,
|
|
})
|
|
);
|
|
});
|
|
finalPresets.sort((a, b) => {
|
|
if (a.status && !b.status) {
|
|
return -1;
|
|
} else {
|
|
return 1;
|
|
}
|
|
});
|
|
return finalPresets;
|
|
}, [availableServers]);
|
|
|
|
const syncLibraries = async () => {
|
|
setIsSyncing(true);
|
|
|
|
const params: { sync: boolean; enable?: string } = {
|
|
sync: true,
|
|
};
|
|
|
|
if (activeLibraries.length > 0) {
|
|
params.enable = activeLibraries.join(',');
|
|
}
|
|
|
|
await axios.get('/api/v1/settings/plex/library', {
|
|
params,
|
|
});
|
|
setIsSyncing(false);
|
|
revalidate();
|
|
};
|
|
|
|
const refreshPresetServers = async () => {
|
|
setIsRefreshingPresets(true);
|
|
let toastId: string | undefined;
|
|
try {
|
|
addToast(
|
|
intl.formatMessage(messages.toastPlexRefresh),
|
|
{
|
|
autoDismiss: false,
|
|
appearance: 'info',
|
|
},
|
|
(id) => {
|
|
toastId = id;
|
|
}
|
|
);
|
|
const response = await axios.get<PlexDevice[]>(
|
|
'/api/v1/settings/plex/devices/servers'
|
|
);
|
|
if (response.data) {
|
|
setAvailableServers(response.data);
|
|
}
|
|
if (toastId) {
|
|
removeToast(toastId);
|
|
}
|
|
addToast(intl.formatMessage(messages.toastPlexRefreshSuccess), {
|
|
autoDismiss: true,
|
|
appearance: 'success',
|
|
});
|
|
} catch (e) {
|
|
if (toastId) {
|
|
removeToast(toastId);
|
|
}
|
|
addToast(intl.formatMessage(messages.toastPlexRefreshFailure), {
|
|
autoDismiss: true,
|
|
appearance: 'error',
|
|
});
|
|
} finally {
|
|
setIsRefreshingPresets(false);
|
|
}
|
|
};
|
|
|
|
const startScan = async () => {
|
|
await axios.post('/api/v1/settings/plex/sync', {
|
|
start: true,
|
|
});
|
|
revalidateSync();
|
|
};
|
|
|
|
const cancelScan = async () => {
|
|
await axios.post('/api/v1/settings/plex/sync', {
|
|
cancel: true,
|
|
});
|
|
revalidateSync();
|
|
};
|
|
|
|
const toggleLibrary = async (libraryId: string) => {
|
|
setIsSyncing(true);
|
|
if (activeLibraries.includes(libraryId)) {
|
|
const params: { enable?: string } = {};
|
|
|
|
if (activeLibraries.length > 1) {
|
|
params.enable = activeLibraries
|
|
.filter((id) => id !== libraryId)
|
|
.join(',');
|
|
}
|
|
|
|
await axios.get('/api/v1/settings/plex/library', {
|
|
params,
|
|
});
|
|
} else {
|
|
await axios.get('/api/v1/settings/plex/library', {
|
|
params: {
|
|
enable: [...activeLibraries, libraryId].join(','),
|
|
},
|
|
});
|
|
}
|
|
setIsSyncing(false);
|
|
revalidate();
|
|
};
|
|
|
|
if (!data && !error) {
|
|
return <LoadingSpinner />;
|
|
}
|
|
return (
|
|
<>
|
|
<PageTitle
|
|
title={[
|
|
intl.formatMessage(messages.plex),
|
|
intl.formatMessage(globalMessages.settings),
|
|
]}
|
|
/>
|
|
<div className="mb-6">
|
|
<h3 className="heading">{intl.formatMessage(messages.plexsettings)}</h3>
|
|
<p className="description">
|
|
{intl.formatMessage(messages.plexsettingsDescription)}
|
|
</p>
|
|
<div className="section">
|
|
<Alert title={intl.formatMessage(messages.settingUpPlex)} type="info">
|
|
{intl.formatMessage(messages.settingUpPlexDescription, {
|
|
RegisterPlexTVLink: function RegisterPlexTVLink(msg) {
|
|
return (
|
|
<a
|
|
href="https://plex.tv"
|
|
className="text-indigo-100 hover:text-white hover:underline"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
{msg}
|
|
</a>
|
|
);
|
|
},
|
|
})}
|
|
</Alert>
|
|
</div>
|
|
</div>
|
|
<Formik
|
|
initialValues={{
|
|
hostname: data?.ip,
|
|
port: data?.port,
|
|
useSsl: data?.useSsl,
|
|
selectedPreset: undefined,
|
|
}}
|
|
validationSchema={PlexSettingsSchema}
|
|
onSubmit={async (values) => {
|
|
let toastId: string | null = null;
|
|
try {
|
|
addToast(
|
|
intl.formatMessage(messages.toastPlexConnecting),
|
|
{
|
|
autoDismiss: false,
|
|
appearance: 'info',
|
|
},
|
|
(id) => {
|
|
toastId = id;
|
|
}
|
|
);
|
|
await axios.post('/api/v1/settings/plex', {
|
|
ip: values.hostname,
|
|
port: Number(values.port),
|
|
useSsl: values.useSsl,
|
|
} as PlexSettings);
|
|
|
|
revalidate();
|
|
setSubmitError(null);
|
|
if (toastId) {
|
|
removeToast(toastId);
|
|
}
|
|
addToast(intl.formatMessage(messages.toastPlexConnectingSuccess), {
|
|
autoDismiss: true,
|
|
appearance: 'success',
|
|
});
|
|
if (onComplete) {
|
|
onComplete();
|
|
}
|
|
} catch (e) {
|
|
if (toastId) {
|
|
removeToast(toastId);
|
|
}
|
|
addToast(intl.formatMessage(messages.toastPlexConnectingFailure), {
|
|
autoDismiss: true,
|
|
appearance: 'error',
|
|
});
|
|
setSubmitError(e.response.data.message);
|
|
}
|
|
}}
|
|
>
|
|
{({
|
|
errors,
|
|
touched,
|
|
values,
|
|
handleSubmit,
|
|
setFieldValue,
|
|
setFieldTouched,
|
|
isSubmitting,
|
|
}) => {
|
|
return (
|
|
<form className="section" onSubmit={handleSubmit}>
|
|
<div className="form-row">
|
|
<label htmlFor="name" className="text-label">
|
|
<div className="flex flex-col">
|
|
<span>{intl.formatMessage(messages.servername)}</span>
|
|
<span className="text-gray-500">
|
|
{intl.formatMessage(messages.servernameTip)}
|
|
</span>
|
|
</div>
|
|
</label>
|
|
<div className="form-input">
|
|
<div className="form-input-field">
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
name="name"
|
|
className="cursor-not-allowed"
|
|
placeholder={intl.formatMessage(
|
|
messages.servernamePlaceholder
|
|
)}
|
|
value={data?.name}
|
|
readOnly
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="form-row">
|
|
<label htmlFor="preset" className="text-label">
|
|
{intl.formatMessage(messages.serverpreset)}
|
|
</label>
|
|
<div className="form-input">
|
|
<div className="form-input-field input-group">
|
|
<select
|
|
id="preset"
|
|
name="preset"
|
|
placeholder={intl.formatMessage(
|
|
messages.serverpresetPlaceholder
|
|
)}
|
|
value={values.selectedPreset}
|
|
disabled={!availableServers || isRefreshingPresets}
|
|
className="rounded-l-only"
|
|
onChange={async (e) => {
|
|
const targPreset =
|
|
availablePresets[Number(e.target.value)];
|
|
if (targPreset) {
|
|
setFieldValue('hostname', targPreset.host);
|
|
setFieldValue('port', targPreset.port);
|
|
setFieldValue('useSsl', targPreset.ssl);
|
|
}
|
|
setFieldTouched('hostname');
|
|
setFieldTouched('port');
|
|
setFieldTouched('useSsl');
|
|
}}
|
|
>
|
|
<option value="manual">
|
|
{availableServers || isRefreshingPresets
|
|
? isRefreshingPresets
|
|
? intl.formatMessage(
|
|
messages.serverpresetRefreshing
|
|
)
|
|
: intl.formatMessage(
|
|
messages.serverpresetManualMessage
|
|
)
|
|
: intl.formatMessage(messages.serverpresetLoad)}
|
|
</option>
|
|
{availablePresets.map((server, index) => (
|
|
<option
|
|
key={`preset-server-${index}`}
|
|
value={index}
|
|
disabled={!server.status}
|
|
>
|
|
{`
|
|
${server.name} (${server.address})
|
|
[${
|
|
server.local
|
|
? intl.formatMessage(messages.serverLocal)
|
|
: intl.formatMessage(messages.serverRemote)
|
|
}]
|
|
${server.status ? '' : '(' + server.message + ')'}
|
|
`}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<button
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
refreshPresetServers();
|
|
}}
|
|
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>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="form-row">
|
|
<label htmlFor="hostname" className="text-label">
|
|
{intl.formatMessage(messages.hostname)}
|
|
</label>
|
|
<div className="form-input">
|
|
<div className="form-input-field">
|
|
<span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md sm:text-sm">
|
|
{values.useSsl ? 'https://' : 'http://'}
|
|
</span>
|
|
<Field
|
|
type="text"
|
|
id="hostname"
|
|
name="hostname"
|
|
placeholder="127.0.0.1"
|
|
className="rounded-r-only"
|
|
/>
|
|
</div>
|
|
{errors.hostname && touched.hostname && (
|
|
<div className="error">{errors.hostname}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="form-row">
|
|
<label htmlFor="port" className="text-label">
|
|
{intl.formatMessage(messages.port)}
|
|
</label>
|
|
<div className="form-input">
|
|
<Field
|
|
type="text"
|
|
id="port"
|
|
name="port"
|
|
placeholder="32400"
|
|
className="short"
|
|
/>
|
|
{errors.port && touched.port && (
|
|
<div className="error">{errors.port}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="form-row">
|
|
<label htmlFor="ssl" className="checkbox-label">
|
|
{intl.formatMessage(messages.ssl)}
|
|
</label>
|
|
<div className="form-input">
|
|
<Field
|
|
type="checkbox"
|
|
id="useSsl"
|
|
name="useSsl"
|
|
onChange={() => {
|
|
setFieldValue('useSsl', !values.useSsl);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{submitError && (
|
|
<div className="mt-6 sm:gap-4 sm:items-start">
|
|
<Alert
|
|
title={intl.formatMessage(
|
|
messages.toastPlexConnectingFailure
|
|
)}
|
|
type="error"
|
|
>
|
|
{submitError}
|
|
</Alert>
|
|
</div>
|
|
)}
|
|
<div className="actions">
|
|
<div className="flex justify-end">
|
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
|
<Button
|
|
buttonType="primary"
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
>
|
|
{isSubmitting
|
|
? intl.formatMessage(messages.saving)
|
|
: intl.formatMessage(messages.save)}
|
|
</Button>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
);
|
|
}}
|
|
</Formik>
|
|
<div className="mt-10 mb-6">
|
|
<h3 className="heading">
|
|
{intl.formatMessage(messages.plexlibraries)}
|
|
</h3>
|
|
<p className="description">
|
|
{intl.formatMessage(messages.plexlibrariesDescription)}
|
|
</p>
|
|
</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>
|
|
{isSyncing
|
|
? intl.formatMessage(messages.scanning)
|
|
: intl.formatMessage(messages.scan)}
|
|
</Button>
|
|
<ul className="grid grid-cols-1 gap-5 mt-6 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
|
{data?.libraries.map((library) => (
|
|
<LibraryItem
|
|
name={library.name}
|
|
isEnabled={library.enabled}
|
|
key={`setting-library-${library.id}`}
|
|
onToggle={() => toggleLibrary(library.id)}
|
|
/>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
<div className="mt-10 mb-6">
|
|
<h3 className="heading">{intl.formatMessage(messages.manualscan)}</h3>
|
|
<p className="description">
|
|
{intl.formatMessage(messages.manualscanDescription)}
|
|
</p>
|
|
</div>
|
|
<div className="section">
|
|
<div className="p-4 bg-gray-800 rounded-md">
|
|
<div className="relative w-full h-8 mb-6 overflow-hidden bg-gray-600 rounded-full">
|
|
{dataSync?.running && (
|
|
<div
|
|
className="h-8 transition-all duration-200 ease-in-out bg-indigo-600"
|
|
style={{
|
|
width: `${Math.round(
|
|
(dataSync.progress / dataSync.total) * 100
|
|
)}%`,
|
|
}}
|
|
/>
|
|
)}
|
|
<div className="absolute inset-0 flex items-center justify-center w-full h-8 text-sm">
|
|
<span>
|
|
{dataSync?.running
|
|
? `${dataSync.progress} of ${dataSync.total}`
|
|
: 'Not running'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col w-full sm:flex-row">
|
|
{dataSync?.running && (
|
|
<>
|
|
{dataSync.currentLibrary && (
|
|
<div className="flex items-center mb-2 mr-0 sm:mb-0 sm:mr-2">
|
|
<Badge>
|
|
{intl.formatMessage(messages.currentlibrary, {
|
|
name: dataSync.currentLibrary.name,
|
|
})}
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center">
|
|
<Badge badgeType="warning">
|
|
{intl.formatMessage(messages.librariesRemaining, {
|
|
count: dataSync.currentLibrary
|
|
? dataSync.libraries.slice(
|
|
dataSync.libraries.findIndex(
|
|
(library) =>
|
|
library.id === dataSync.currentLibrary?.id
|
|
) + 1
|
|
).length
|
|
: 0,
|
|
})}
|
|
</Badge>
|
|
</div>
|
|
</>
|
|
)}
|
|
<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>
|
|
{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>
|
|
{intl.formatMessage(messages.cancelscan)}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default SettingsPlex;
|