8
resources/client/admin/settings/app-settings-nav-config.ts
Executable file
8
resources/client/admin/settings/app-settings-nav-config.ts
Executable file
@@ -0,0 +1,8 @@
|
||||
import {SettingsNavItem} from '@common/admin/settings/settings-nav-config';
|
||||
import {message} from '@common/i18n/message';
|
||||
|
||||
export const AppSettingsNavConfig: SettingsNavItem[] = [
|
||||
{label: message('Local search'), to: 'search'},
|
||||
{label: message('Content'), to: 'content'},
|
||||
{label: message('Videos'), to: 'videos'},
|
||||
];
|
||||
19
resources/client/admin/settings/app-settings-routes.tsx
Executable file
19
resources/client/admin/settings/app-settings-routes.tsx
Executable file
@@ -0,0 +1,19 @@
|
||||
import {RouteObject} from 'react-router-dom';
|
||||
import {VideoSettings} from '@app/admin/settings/video-settings';
|
||||
import {ContentSettings} from '@app/admin/settings/content-settings/content-settings';
|
||||
import {SearchSettings} from '@common/admin/settings/pages/search-settings/search-settings';
|
||||
|
||||
export const AppSettingsRoutes: RouteObject[] = [
|
||||
{
|
||||
path: 'search',
|
||||
element: <SearchSettings />,
|
||||
},
|
||||
{
|
||||
path: 'videos',
|
||||
element: <VideoSettings />,
|
||||
},
|
||||
{
|
||||
path: 'content',
|
||||
element: <ContentSettings />,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,152 @@
|
||||
import {Fragment} from 'react';
|
||||
import {FormSwitch} from '@common/ui/forms/toggle/switch';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {FormSelect} from '@common/ui/forms/select/select';
|
||||
import {Item} from '@common/ui/forms/listbox/item';
|
||||
import {SettingsSeparator} from '@common/admin/settings/settings-separator';
|
||||
import {useFormContext} from 'react-hook-form';
|
||||
import {AdminSettings} from '@common/admin/settings/admin-settings';
|
||||
import {SettingsErrorGroup} from '@common/admin/settings/settings-error-group';
|
||||
import {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';
|
||||
import {useValueLists} from '@common/http/value-lists';
|
||||
|
||||
export function ContentSettingsAutomationPanel() {
|
||||
const {watch} = useFormContext<AdminSettings>();
|
||||
return (
|
||||
<Fragment>
|
||||
<SearchMethodSelect />
|
||||
<FormSwitch
|
||||
className="mb-24"
|
||||
name="client.content.title_provider"
|
||||
value="tmdb"
|
||||
description={
|
||||
<Trans message="This will automatically import, and periodically update, all metadata available on TheMovieDB about the title when user visits that title's page." />
|
||||
}
|
||||
>
|
||||
<Trans message="Title automation" />
|
||||
</FormSwitch>
|
||||
<FormSwitch
|
||||
className="mb-24"
|
||||
name="client.content.force_season_update"
|
||||
value="tmdb"
|
||||
description={
|
||||
<Trans message="When this is enabled, season episodes will be automatically updated, even if title automation is disabled." />
|
||||
}
|
||||
>
|
||||
<Trans message="Always update seasons" />
|
||||
</FormSwitch>
|
||||
<SettingsSeparator />
|
||||
<FormSwitch
|
||||
className="mb-24"
|
||||
name="client.content.people_provider"
|
||||
value="tmdb"
|
||||
description={
|
||||
<Trans message="This will automatically import, and periodically update, all metadata available on TheMovieDB about a person, when user visits that person's page." />
|
||||
}
|
||||
>
|
||||
<Trans message="People automation" />
|
||||
</FormSwitch>
|
||||
{watch('client.content.people_provider') === 'tmdb' && (
|
||||
<FormSwitch
|
||||
className="mb-24"
|
||||
name="client.content.automate_filmography"
|
||||
description={
|
||||
<Trans message="Whether full filmograpy for a person should be imported from TheMovieDB when auto updating the person metadata." />
|
||||
}
|
||||
>
|
||||
<Trans message="Full filmography" />
|
||||
</FormSwitch>
|
||||
)}
|
||||
<TmdbFields />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchMethodSelect() {
|
||||
return (
|
||||
<FormSelect
|
||||
className="mb-24"
|
||||
name="client.content.search_provider"
|
||||
selectionMode="single"
|
||||
label={<Trans message="Search method" />}
|
||||
description={
|
||||
<Trans message="Which method should be used for user facing search on the site." />
|
||||
}
|
||||
>
|
||||
<Item
|
||||
value="tmdb"
|
||||
description={
|
||||
<Trans message="Search on the site will directly connect to, and search TheMovieDB. Any movie, series and artist available on TheMovieDB will be discoverable via search, without needing to import or create it first." />
|
||||
}
|
||||
>
|
||||
<Trans message="TheMovieDB" />
|
||||
</Item>
|
||||
<Item
|
||||
value="local"
|
||||
description={
|
||||
<Trans message="Will only search content that was created or imported from admin area. This can be further configured from 'Local search' settings page." />
|
||||
}
|
||||
>
|
||||
<Trans message="Local" />
|
||||
</Item>
|
||||
<Item
|
||||
value="all"
|
||||
description={
|
||||
<Trans message="Will combine search results from both 'Local' and 'TheMovieDB' methods. If there are identical matches, local results will be preferred." />
|
||||
}
|
||||
>
|
||||
<Trans message="Local and TheMovieDB" />
|
||||
</Item>
|
||||
</FormSelect>
|
||||
);
|
||||
}
|
||||
|
||||
function TmdbFields() {
|
||||
const {data} = useValueLists(['tmdbLanguages']);
|
||||
const {watch: w} = useFormContext<AdminSettings>();
|
||||
const shouldShow = [
|
||||
w('client.content.people_provider'),
|
||||
w('client.content.title_provider'),
|
||||
w('client.content.search_provider'),
|
||||
].some(provider => `${provider}`.toLowerCase().includes('tmdb'));
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsErrorGroup name="tmdb_group" separatorBottom={false}>
|
||||
{isInvalid => (
|
||||
<Fragment>
|
||||
<FormTextField
|
||||
invalid={isInvalid}
|
||||
name="server.tmdb_api_key"
|
||||
label={<Trans message="TheMovieDB API Key" />}
|
||||
className="mb-24"
|
||||
required
|
||||
/>
|
||||
<FormSelect
|
||||
className="mb-24"
|
||||
selectionMode="single"
|
||||
showSearchField
|
||||
invalid={isInvalid}
|
||||
name="client.tmdb.language"
|
||||
label={<Trans message="TheMovieDB language" />}
|
||||
description={
|
||||
<Trans message="In what language should content be fetched from TMDb. If translation is not available, data will be in original language for that movie or series." />
|
||||
}
|
||||
>
|
||||
{data?.tmdbLanguages.map(({code, name}) => (
|
||||
<Item value={code} key={code}>
|
||||
{name}
|
||||
</Item>
|
||||
))}
|
||||
</FormSelect>
|
||||
<FormSwitch name="client.tmdb.includeAdult">
|
||||
<Trans message="Import adult content" />
|
||||
</FormSwitch>
|
||||
</Fragment>
|
||||
)}
|
||||
</SettingsErrorGroup>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import {useFormContext} from 'react-hook-form';
|
||||
import {AdminSettings} from '@common/admin/settings/admin-settings';
|
||||
import {Fragment} from 'react';
|
||||
import {FormSwitch} from '@common/ui/forms/toggle/switch';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {FormSelect} from '@common/ui/forms/select/select';
|
||||
import {Item} from '@common/ui/forms/listbox/item';
|
||||
|
||||
export function ContentSettingsGeneralPanel() {
|
||||
const {watch} = useFormContext<AdminSettings>();
|
||||
return (
|
||||
<Fragment>
|
||||
<SortingMethodSelect />
|
||||
<FormSwitch
|
||||
className="mb-24"
|
||||
name="client.titles.enable_reviews"
|
||||
description={
|
||||
<Trans
|
||||
message={
|
||||
'Enable or disable all review functionality across the site.'
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Trans message="Enable reviews" />
|
||||
</FormSwitch>
|
||||
<FormSwitch
|
||||
className="mb-24"
|
||||
name="client.titles.enable_comments"
|
||||
description={
|
||||
<Trans
|
||||
message={
|
||||
'Enable or disable all comment functionality across the site.'
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Trans message="Enable comments" />
|
||||
</FormSwitch>
|
||||
{watch('client.titles.enable_comments') && (
|
||||
<FormSwitch
|
||||
name="client.comments.per_video"
|
||||
description={
|
||||
<Trans
|
||||
message={
|
||||
'When enabled, individual videos will have their own separate comment section (if there are multiple videos), otherwise comments will be shared by all videos for the same title.'
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Trans message="Per video comments" />
|
||||
</FormSwitch>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function SortingMethodSelect() {
|
||||
return (
|
||||
<FormSelect
|
||||
className="mb-24"
|
||||
name="server.rating_column"
|
||||
label={<Trans message="Rating used for sorting" />}
|
||||
selectionMode="single"
|
||||
description={
|
||||
<Trans
|
||||
message="When ordering titles by rating, should local user rating or TheMovieDB rating average be
|
||||
used."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Item value="tmdb_vote_average">
|
||||
<Trans message="TheMovieDB" />
|
||||
</Item>
|
||||
<Item value="local_vote_average">
|
||||
<Trans message="Local (Ratings and reviews from site users)" />
|
||||
</Item>
|
||||
</FormSelect>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import React, {Fragment, ReactNode, useRef, useState} from 'react';
|
||||
import {DragPreviewRenderer} from '@common/ui/interactions/dnd/use-draggable';
|
||||
import {useFormContext} from 'react-hook-form';
|
||||
import {AdminSettingsWithFiles} from '@common/admin/settings/requests/update-admin-settings';
|
||||
import {moveItemInNewArray} from '@common/utils/array/move-item-in-new-array';
|
||||
import {IconButton} from '@common/ui/buttons/icon-button';
|
||||
import {DragHandleIcon} from '@common/icons/material/DragHandle';
|
||||
import {Checkbox} from '@common/ui/forms/toggle/checkbox';
|
||||
import {DragPreview} from '@common/ui/interactions/dnd/drag-preview';
|
||||
import clsx from 'clsx';
|
||||
import {TitlePageSections} from '@app/titles/pages/title-page/sections/title-page-sections';
|
||||
import {MessageDescriptor} from '@common/i18n/message-descriptor';
|
||||
import {AdminSettings} from '@common/admin/settings/admin-settings';
|
||||
import {useSortable} from '@common/ui/interactions/dnd/sortable/use-sortable';
|
||||
|
||||
interface SectionItem {
|
||||
name: (typeof TitlePageSections)[number];
|
||||
title: MessageDescriptor;
|
||||
}
|
||||
|
||||
const defaultItems: SectionItem[] = [
|
||||
{name: 'episodes', title: {message: 'Episode grid'}},
|
||||
{name: 'seasons', title: {message: 'Season grid'}},
|
||||
{name: 'videos', title: {message: 'Video grid'}},
|
||||
{name: 'images', title: {message: 'Image grid'}},
|
||||
{name: 'reviews', title: {message: 'Reviews'}},
|
||||
{name: 'cast', title: {message: 'Cast grid'}},
|
||||
{name: 'related', title: {message: 'Related titles'}},
|
||||
];
|
||||
|
||||
export function ContentSettingsTitlePagePanel() {
|
||||
const {getValues, setValue} = useFormContext<AdminSettings>();
|
||||
const getSavedValue = (): string[] => {
|
||||
return getValues('client.title_page.sections') || [];
|
||||
};
|
||||
|
||||
const [items, setItems] = useState(() => {
|
||||
const savedValue = getSavedValue();
|
||||
const sortFn = (x: string) =>
|
||||
savedValue.includes(x) ? savedValue.indexOf(x) : savedValue.length;
|
||||
return [...defaultItems].sort((a, b) => sortFn(a.name) - sortFn(b.name));
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-14 text-sm">
|
||||
<Trans message="Title page sections" />
|
||||
<div className="text-xs text-muted">
|
||||
<Trans message="Select which sections should appear on title page and in which order." />
|
||||
</div>
|
||||
</div>
|
||||
{items.map((section, index) => (
|
||||
<ListItemLayout
|
||||
items={items}
|
||||
isFirst={index === 0}
|
||||
key={section.name}
|
||||
section={section}
|
||||
title={<Trans {...section.title} />}
|
||||
onToggle={(section, checked) => {
|
||||
const savedValue = getSavedValue();
|
||||
const newValue = checked
|
||||
? [...savedValue, section.name]
|
||||
: savedValue.filter(x => x !== section.name);
|
||||
setValue('client.title_page.sections', newValue as any);
|
||||
}}
|
||||
onSortEnd={(oldIndex, newIndex) => {
|
||||
const sortedItems = moveItemInNewArray(items, oldIndex, newIndex);
|
||||
setItems(sortedItems);
|
||||
const savedValue = getSavedValue();
|
||||
const newValue = sortedItems
|
||||
.filter(x => savedValue.includes(x.name))
|
||||
.map(x => x.name);
|
||||
setValue('client.title_page.sections', newValue);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ListItemLayoutProps {
|
||||
isFirst: boolean;
|
||||
items: SectionItem[];
|
||||
section: SectionItem;
|
||||
title: ReactNode;
|
||||
onSortEnd: (oldIndex: number, newIndex: number) => void;
|
||||
onToggle: (section: SectionItem, checked: boolean) => void;
|
||||
}
|
||||
function ListItemLayout({
|
||||
isFirst,
|
||||
title,
|
||||
items,
|
||||
section,
|
||||
onSortEnd,
|
||||
onToggle,
|
||||
}: ListItemLayoutProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const previewRef = useRef<DragPreviewRenderer>(null);
|
||||
const {watch} = useFormContext<AdminSettingsWithFiles>();
|
||||
|
||||
const savedValue = watch('client.title_page.sections') || [];
|
||||
const isChecked = savedValue.includes(section.name);
|
||||
|
||||
const {sortableProps, dragHandleRef} = useSortable({
|
||||
ref,
|
||||
item: section,
|
||||
items,
|
||||
type: 'titlePageSections',
|
||||
preview: previewRef,
|
||||
strategy: 'line',
|
||||
onSortEnd,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex w-full items-center gap-8 border-b py-6',
|
||||
isFirst && 'border-t border-t-transparent',
|
||||
)}
|
||||
ref={ref}
|
||||
{...sortableProps}
|
||||
>
|
||||
<IconButton ref={dragHandleRef}>
|
||||
<DragHandleIcon />
|
||||
</IconButton>
|
||||
<div className="flex-auto">
|
||||
<div className="text-sm">{title}</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={isChecked}
|
||||
onChange={() => {
|
||||
onToggle(section, !isChecked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<TabDragPreview title={title} ref={previewRef} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
interface DragPreviewProps {
|
||||
title: ReactNode;
|
||||
}
|
||||
const TabDragPreview = React.forwardRef<DragPreviewRenderer, DragPreviewProps>(
|
||||
({title}, ref) => {
|
||||
return (
|
||||
<DragPreview ref={ref}>
|
||||
{() => (
|
||||
<div className="rounded bg-chip p-8 text-sm shadow">{title}</div>
|
||||
)}
|
||||
</DragPreview>
|
||||
);
|
||||
},
|
||||
);
|
||||
45
resources/client/admin/settings/content-settings/content-settings.tsx
Executable file
45
resources/client/admin/settings/content-settings/content-settings.tsx
Executable file
@@ -0,0 +1,45 @@
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {SettingsPanel} from '@common/admin/settings/settings-panel';
|
||||
import {Tabs} from '@common/ui/tabs/tabs';
|
||||
import {TabList} from '@common/ui/tabs/tab-list';
|
||||
import {Tab} from '@common/ui/tabs/tab';
|
||||
import {TabPanel, TabPanels} from '@common/ui/tabs/tab-panels';
|
||||
import {ContentSettingsGeneralPanel} from '@app/admin/settings/content-settings/content-settings-general-panel';
|
||||
import {ContentSettingsAutomationPanel} from '@app/admin/settings/content-settings/content-settings-automation-panel';
|
||||
import {ContentSettingsTitlePagePanel} from '@app/admin/settings/content-settings/content-settings-title-page-panel';
|
||||
|
||||
export function ContentSettings() {
|
||||
return (
|
||||
<SettingsPanel
|
||||
title={<Trans message="Content" />}
|
||||
description={
|
||||
<Trans message="Control how content is displayed across the site." />
|
||||
}
|
||||
>
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab width="min-w-132">
|
||||
<Trans message="General" />
|
||||
</Tab>
|
||||
<Tab width="min-w-132">
|
||||
<Trans message="Automation" />
|
||||
</Tab>
|
||||
<Tab width="min-w-132">
|
||||
<Trans message="Title page" />
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels className="pt-24">
|
||||
<TabPanel>
|
||||
<ContentSettingsGeneralPanel />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ContentSettingsAutomationPanel />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ContentSettingsTitlePagePanel />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
122
resources/client/admin/settings/video-settings.tsx
Executable file
122
resources/client/admin/settings/video-settings.tsx
Executable file
@@ -0,0 +1,122 @@
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {FormSelect} from '@common/ui/forms/select/select';
|
||||
import {Item} from '@common/ui/forms/listbox/item';
|
||||
import {SettingsPanel} from '@common/admin/settings/settings-panel';
|
||||
import {FormSwitch} from '@common/ui/forms/toggle/switch';
|
||||
import {JsonChipField} from '@common/admin/settings/json-chip-field';
|
||||
import {useTrans} from '@common/i18n/use-trans';
|
||||
|
||||
export function VideoSettings() {
|
||||
const {trans} = useTrans();
|
||||
return (
|
||||
<SettingsPanel
|
||||
title={<Trans message="Video and streaming" />}
|
||||
description={
|
||||
<Trans message="Control how videos are played and displayed on the site." />
|
||||
}
|
||||
>
|
||||
<ShownVideoTypeSelect />
|
||||
<SortingMethodSelect />
|
||||
<FormSwitch
|
||||
className="mb-24"
|
||||
name="client.streaming.prefer_full"
|
||||
description={
|
||||
<Trans
|
||||
message={
|
||||
'When user clicks on "play" buttons across the site play full movie or episode instead of trailers and clips.'
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Trans message="Prefer full videos" />
|
||||
</FormSwitch>
|
||||
<FormSwitch
|
||||
className="mb-24"
|
||||
name="client.streaming.show_video_selector"
|
||||
description={
|
||||
<Trans message="Show alternative videos on the watch page." />
|
||||
}
|
||||
>
|
||||
<Trans message="Alternative videos" />
|
||||
</FormSwitch>
|
||||
<FormSwitch
|
||||
className="mb-24"
|
||||
name="client.streaming.show_header_play"
|
||||
description={
|
||||
<Trans message="Whether play button should be shown on main title header." />
|
||||
}
|
||||
>
|
||||
<Trans message="Header play button" />
|
||||
</FormSwitch>
|
||||
<JsonChipField
|
||||
className="mb-24"
|
||||
label={<Trans message="Possible video qualities" />}
|
||||
name="client.streaming.qualities"
|
||||
placeholder={trans({message: 'Add another...'})}
|
||||
/>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
||||
function SortingMethodSelect() {
|
||||
return (
|
||||
<FormSelect
|
||||
className="mb-24"
|
||||
name="client.streaming.default_sort"
|
||||
label={<Trans message="Video sorting" />}
|
||||
selectionMode="single"
|
||||
description={
|
||||
<Trans message="When multiple videos are shown on the page, how should they be sorted by default." />
|
||||
}
|
||||
>
|
||||
<Item value="order:asc">
|
||||
<Trans message="Manual (order assigned manually in admin area)" />
|
||||
</Item>
|
||||
<Item value="created_at:desc">
|
||||
<Trans message="Date added" />
|
||||
</Item>
|
||||
<Item value="name:asc">
|
||||
<Trans message="Name (a-z)" />
|
||||
</Item>
|
||||
<Item value="Language:asc">
|
||||
<Trans message="Language (a-z)" />
|
||||
</Item>
|
||||
<Item value="reports:asc">
|
||||
<Trans message="Reports (videos with less reports first)" />
|
||||
</Item>
|
||||
<Item value="score:desc">
|
||||
<Trans message="Score (most liked videos first)" />
|
||||
</Item>
|
||||
</FormSelect>
|
||||
);
|
||||
}
|
||||
|
||||
function ShownVideoTypeSelect() {
|
||||
return (
|
||||
<FormSelect
|
||||
className="mb-24"
|
||||
name="client.streaming.video_panel_content"
|
||||
label={<Trans message="Shown videos" />}
|
||||
selectionMode="single"
|
||||
description={
|
||||
<Trans message="What type of videos should be shown in title and episode pages (if there is more then one video attached)." />
|
||||
}
|
||||
>
|
||||
<Item value="all">
|
||||
<Trans message="All videos" />
|
||||
</Item>
|
||||
<Item value="full">
|
||||
<Trans message="Full movies and episodes" />
|
||||
</Item>
|
||||
<Item value="short">
|
||||
<Trans message="Short videos (everything except full movies & episodes)" />
|
||||
</Item>
|
||||
<Item value="trailer">
|
||||
<Trans message="Trailers" />
|
||||
</Item>
|
||||
<Item value="clip">
|
||||
<Trans message="Clips" />
|
||||
</Item>
|
||||
</FormSelect>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user