Skip to content

Commit

Permalink
feat(frontend): plex settings page
Browse files Browse the repository at this point in the history
  • Loading branch information
sct committed Oct 30, 2020
1 parent 33da7e9 commit 47714b6
Show file tree
Hide file tree
Showing 7 changed files with 364 additions and 1 deletion.
2 changes: 2 additions & 0 deletions overseerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,8 @@ paths:
nullable: true
- in: query
name: enable
explode: false
allowReserved: true
description: Comma separated list of libraries to enable. Any libraries not passed will be disabled!
schema:
type: string
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"express": "^4.17.1",
"express-openapi-validator": "^3.16.15",
"express-session": "^1.17.1",
"formik": "^2.2.1",
"intl": "^1.2.5",
"lodash": "^4.17.20",
"next": "9.5.4",
Expand Down
2 changes: 1 addition & 1 deletion server/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface Library {
enabled: boolean;
}

interface PlexSettings {
export interface PlexSettings {
name: string;
machineId?: string;
ip: string;
Expand Down
89 changes: 89 additions & 0 deletions src/components/Settings/LibraryItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';

interface LibraryItemProps {
isEnabled?: boolean;
name: string;
onToggle: () => void;
}

const LibraryItem: React.FC<LibraryItemProps> = ({
isEnabled,
name,
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-cool-gray-700 bg-cool-gray-600 rounded-md truncate">
<div className="flex-1 px-4 py-6 text-sm leading-5 truncate cursor-default">
{name}
</div>
<div className="flex-shrink-0 pr-2">
{/* <!-- On: "bg-indigo-600", Off: "bg-gray-200" --> */}
<span
role="checkbox"
tabIndex={0}
aria-checked={isEnabled}
onClick={() => onToggle()}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onToggle();
}
}}
className={`${
isEnabled ? 'bg-indigo-600' : 'bg-cool-gray-700'
} relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:shadow-outline`}
>
{/* <!-- On: "translate-x-5", Off: "translate-x-0" --> */}
<span
aria-hidden="true"
className={`${
isEnabled ? 'translate-x-5' : 'translate-x-0'
} relative inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-200`}
>
{/* <!-- On: "opacity-0 ease-out duration-100", Off: "opacity-100 ease-in duration-200" --> */}
<span
className={`${
isEnabled
? 'opacity-0 ease-out duration-100'
: '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>
</span>
{/* <!-- On: "opacity-100 ease-in duration-200", Off: "opacity-0 ease-out duration-100" --> */}
<span
className={`${
isEnabled
? 'opacity-100 ease-in duration-200'
: '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>
</span>
</span>
</span>
</div>
</div>
</li>
);
};

export default LibraryItem;
210 changes: 210 additions & 0 deletions src/components/Settings/SettingsPlex.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import React, { useState } from 'react';
import LoadingSpinner from '../Common/LoadingSpinner';
import type { PlexSettings } from '../../../server/lib/settings';
import useSWR from 'swr';
import { useFormik } from 'formik';
import Button from '../Common/Button';
import axios from 'axios';
import LibraryItem from './LibraryItem';

const SettingsPlex: React.FC = () => {
const { data, error, revalidate } = useSWR<PlexSettings>(
'/api/v1/settings/plex'
);
const [isSyncing, setIsSyncing] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const formik = useFormik({
initialValues: {
hostname: data?.ip,
port: data?.port,
},
onSubmit: async (values) => {
setIsUpdating(true);
try {
await axios.post('/api/v1/settings/plex', {
ip: values.hostname,
port: values.port,
} as PlexSettings);

revalidate();
} catch (e) {
setSubmitError(e.message);
} finally {
setIsUpdating(false);
}
},
});

const activeLibraries =
data?.libraries
.filter((library) => library.enabled)
.map((library) => library.id) ?? [];

const syncLibraries = async () => {
setIsSyncing(true);
await axios.get('/api/v1/settings/plex/library', {
params: {
sync: true,
enable:
activeLibraries.length > 0 ? activeLibraries.join(',') : undefined,
},
});
setIsSyncing(false);
revalidate();
};

const toggleLibrary = async (libraryId: string) => {
setIsSyncing(true);
if (activeLibraries.includes(libraryId)) {
await axios.get('/api/v1/settings/plex/library', {
params: {
enable:
activeLibraries.length > 0
? activeLibraries.filter((id) => id !== libraryId).join(',')
: undefined,
},
});
} else {
await axios.get('/api/v1/settings/plex/library', {
params: {
enable: [...activeLibraries, libraryId].join(','),
},
});
}
setIsSyncing(false);
revalidate();
};

if (!data && !error) {
return <LoadingSpinner />;
}
return (
<>
<div>
<h3 className="text-lg leading-6 font-medium text-cool-gray-200">
Plex Settings
</h3>
<p className="mt-1 max-w-2xl text-sm leading-5 text-cool-gray-500">
Configure the settings for your Plex server. Overseerr uses your Plex
server to scan your library at an interval and see what content is
available.
</p>
</div>
<form onSubmit={formik.handleSubmit}>
<div className="mt-6 sm:mt-5">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<label
htmlFor="name"
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
>
Server Name (Automatically set)
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<input
id="name"
name="name"
placeholder="Plex Server Name (will be set automatically)"
value={data?.name}
readOnly
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-cool-gray-700 border border-cool-gray-500"
/>
</div>
</div>
</div>
<div className="mt-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<label
htmlFor="hostname"
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
>
Hostname/IP
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<input
id="hostname"
name="hostname"
placeholder="127.0.0.1"
value={formik.values.hostname}
onChange={formik.handleChange}
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-cool-gray-700 border border-cool-gray-500"
/>
</div>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label
htmlFor="port"
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
>
Port
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg rounded-md shadow-sm sm:max-w-xs">
<input
id="port"
name="port"
placeholder="32400"
value={formik.values.port}
onChange={formik.handleChange}
className="form-input block w-24 transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-cool-gray-700 border border-cool-gray-500"
/>
</div>
</div>
</div>
</div>
<div className="mt-8 border-t border-cool-gray-700 pt-5">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">
<Button buttonType="primary" type="submit" disabled={isUpdating}>
{isUpdating ? 'Saving...' : 'Save Changes'}
</Button>
</span>
</div>
</div>
<div className="mt-10">
<h3 className="text-lg leading-6 font-medium text-cool-gray-200">
Plex Libraries
</h3>
<p className="mt-1 max-w-2xl text-sm leading-5 text-cool-gray-500">
These are the libraries Overseerr will scan for titles. If you see
no libraries listed, you will need to run at least one sync by
clicking the button below. You must first configure and save your
plex connection settings before you will be able to retrieve your
libraries.
</p>
<div className="mt-6">
<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 ? 'Syncing...' : 'Sync Plex Libraries'}
</Button>
</div>
<ul className="mt-6 grid grid-cols-1 gap-5 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>
</form>
</>
);
};

export default SettingsPlex;
14 changes: 14 additions & 0 deletions src/pages/settings/plex.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import type { NextPage } from 'next';
import SettingsLayout from '../../components/Settings/SettingsLayout';
import SettingsPlex from '../../components/Settings/SettingsPlex';

const PlexSettingsPage: NextPage = () => {
return (
<SettingsLayout>
<SettingsPlex />
</SettingsLayout>
);
};

export default PlexSettingsPage;

0 comments on commit 47714b6

Please sign in to comment.