diff options
Diffstat (limited to 'frontend/src/components')
-rw-r--r-- | frontend/src/components/Search.tsx | 41 | ||||
-rw-r--r-- | frontend/src/components/TextPopover.tsx | 2 | ||||
-rw-r--r-- | frontend/src/components/async/MutateAction.tsx | 1 | ||||
-rw-r--r-- | frontend/src/components/async/MutateButton.tsx | 1 | ||||
-rw-r--r-- | frontend/src/components/async/QueryOverlay.tsx | 2 | ||||
-rw-r--r-- | frontend/src/components/bazarr/AudioList.tsx | 3 | ||||
-rw-r--r-- | frontend/src/components/forms/MovieUploadForm.tsx | 92 | ||||
-rw-r--r-- | frontend/src/components/forms/ProfileEditForm.module.scss | 8 | ||||
-rw-r--r-- | frontend/src/components/forms/ProfileEditForm.tsx | 31 | ||||
-rw-r--r-- | frontend/src/components/forms/SeriesUploadForm.tsx | 103 | ||||
-rw-r--r-- | frontend/src/components/forms/uploadFormSelectorTypes.tsx | 16 | ||||
-rw-r--r-- | frontend/src/components/inputs/Selector.tsx | 5 |
12 files changed, 210 insertions, 95 deletions
diff --git a/frontend/src/components/Search.tsx b/frontend/src/components/Search.tsx index b506afee3..c0dde3bef 100644 --- a/frontend/src/components/Search.tsx +++ b/frontend/src/components/Search.tsx @@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom"; import { Autocomplete, ComboboxItem, OptionsFilter, Text } from "@mantine/core"; import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { chain, includes } from "lodash"; import { useServerSearch } from "@/apis/hooks"; import { useDebouncedValue } from "@/utilities"; @@ -15,23 +16,45 @@ function useSearch(query: string) { const debouncedQuery = useDebouncedValue(query, 500); const { data } = useServerSearch(debouncedQuery, debouncedQuery.length >= 0); + const duplicates = chain(data) + .groupBy((item) => `${item.title} (${item.year})`) + .filter((group) => group.length > 1) + .map((group) => `${group[0].title} (${group[0].year})`) + .value(); + return useMemo<SearchResultItem[]>( () => data?.map((v) => { - let link: string; - if (v.sonarrSeriesId) { - link = `/series/${v.sonarrSeriesId}`; - } else if (v.radarrId) { - link = `/movies/${v.radarrId}`; - } else { + const { link, displayName } = (() => { + const hasDuplicate = includes(duplicates, `${v.title} (${v.year})`); + + if (v.sonarrSeriesId) { + return { + link: `/series/${v.sonarrSeriesId}`, + displayName: hasDuplicate + ? `${v.title} (${v.year}) (S)` + : `${v.title} (${v.year})`, + }; + } + + if (v.radarrId) { + return { + link: `/movies/${v.radarrId}`, + displayName: hasDuplicate + ? `${v.title} (${v.year}) (M)` + : `${v.title} (${v.year})`, + }; + } + throw new Error("Unknown search result"); - } + })(); + return { - value: `${v.title} (${v.year})`, + value: displayName, link, }; }) ?? [], - [data], + [data, duplicates], ); } diff --git a/frontend/src/components/TextPopover.tsx b/frontend/src/components/TextPopover.tsx index 974c0d0c0..03dd58700 100644 --- a/frontend/src/components/TextPopover.tsx +++ b/frontend/src/components/TextPopover.tsx @@ -25,7 +25,7 @@ const TextPopover: FunctionComponent<TextPopoverProps> = ({ opened={hovered} label={text} {...tooltip} - style={{ textWrap: "pretty" }} + style={{ textWrap: "wrap" }} > <div ref={ref}>{children}</div> </Tooltip> diff --git a/frontend/src/components/async/MutateAction.tsx b/frontend/src/components/async/MutateAction.tsx index 6fff0dbb7..92c102ea9 100644 --- a/frontend/src/components/async/MutateAction.tsx +++ b/frontend/src/components/async/MutateAction.tsx @@ -16,7 +16,6 @@ type MutateActionProps<DATA, VAR> = Omit< function MutateAction<DATA, VAR>({ mutation, - noReset, onSuccess, onError, args, diff --git a/frontend/src/components/async/MutateButton.tsx b/frontend/src/components/async/MutateButton.tsx index 9197e2d50..908c2dfda 100644 --- a/frontend/src/components/async/MutateButton.tsx +++ b/frontend/src/components/async/MutateButton.tsx @@ -15,7 +15,6 @@ type MutateButtonProps<DATA, VAR> = Omit< function MutateButton<DATA, VAR>({ mutation, - noReset, onSuccess, onError, args, diff --git a/frontend/src/components/async/QueryOverlay.tsx b/frontend/src/components/async/QueryOverlay.tsx index 2a5848cf2..1672989ff 100644 --- a/frontend/src/components/async/QueryOverlay.tsx +++ b/frontend/src/components/async/QueryOverlay.tsx @@ -12,7 +12,7 @@ interface QueryOverlayProps { const QueryOverlay: FunctionComponent<QueryOverlayProps> = ({ children, global = false, - result: { isLoading, isError, error }, + result: { isLoading }, }) => { return ( <LoadingProvider value={isLoading}> diff --git a/frontend/src/components/bazarr/AudioList.tsx b/frontend/src/components/bazarr/AudioList.tsx index f1af7ff3c..f0dc07c0d 100644 --- a/frontend/src/components/bazarr/AudioList.tsx +++ b/frontend/src/components/bazarr/AudioList.tsx @@ -1,6 +1,7 @@ import { FunctionComponent } from "react"; import { Badge, BadgeProps, Group, GroupProps } from "@mantine/core"; import { BuildKey } from "@/utilities"; +import { normalizeAudioLanguage } from "@/utilities/languages"; export type AudioListProps = GroupProps & { audios: Language.Info[]; @@ -16,7 +17,7 @@ const AudioList: FunctionComponent<AudioListProps> = ({ <Group gap="xs" {...group}> {audios.map((audio, idx) => ( <Badge color="blue" key={BuildKey(idx, audio.code2)} {...badgeProps}> - {audio.name} + {normalizeAudioLanguage(audio.name)} </Badge> ))} </Group> diff --git a/frontend/src/components/forms/MovieUploadForm.tsx b/frontend/src/components/forms/MovieUploadForm.tsx index 8e318d7ad..f7f8f47c5 100644 --- a/frontend/src/components/forms/MovieUploadForm.tsx +++ b/frontend/src/components/forms/MovieUploadForm.tsx @@ -1,9 +1,9 @@ -import { FunctionComponent, useEffect, useMemo } from "react"; +import React, { FunctionComponent, useEffect, useMemo } from "react"; import { Button, - Checkbox, Divider, MantineColor, + Select, Stack, Text, } from "@mantine/core"; @@ -17,8 +17,9 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ColumnDef } from "@tanstack/react-table"; -import { isString } from "lodash"; +import { isString, uniqBy } from "lodash"; import { useMovieSubtitleModification } from "@/apis/hooks"; +import { subtitlesTypeOptions } from "@/components/forms/uploadFormSelectorTypes"; import { Action, Selector } from "@/components/inputs"; import SimpleTable from "@/components/tables/SimpleTable"; import TextPopover from "@/components/TextPopover"; @@ -88,7 +89,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({ const languages = useProfileItemsToLanguages(profile); const languageOptions = useSelectorOptions( - languages, + uniqBy(languages, "code2"), (v) => v.name, (v) => v.code2, ); @@ -208,34 +209,6 @@ const MovieUploadForm: FunctionComponent<Props> = ({ }, }, { - header: "Forced", - accessorKey: "forced", - cell: ({ row: { original, index } }) => { - return ( - <Checkbox - checked={original.forced} - onChange={({ currentTarget: { checked } }) => { - action.mutate(index, { ...original, forced: checked }); - }} - ></Checkbox> - ); - }, - }, - { - header: "HI", - accessorKey: "hi", - cell: ({ row: { original, index } }) => { - return ( - <Checkbox - checked={original.hi} - onChange={({ currentTarget: { checked } }) => { - action.mutate(index, { ...original, hi: checked }); - }} - ></Checkbox> - ); - }, - }, - { header: "Language", accessorKey: "language", cell: ({ row: { original, index } }) => { @@ -252,6 +225,61 @@ const MovieUploadForm: FunctionComponent<Props> = ({ }, }, { + header: () => ( + <Selector + options={subtitlesTypeOptions} + value={null} + placeholder="Type" + onChange={(value) => { + if (value) { + action.update((item) => { + switch (value) { + case "hi": + return { ...item, hi: true, forced: false }; + case "forced": + return { ...item, hi: false, forced: true }; + case "normal": + return { ...item, hi: false, forced: false }; + default: + return item; + } + }); + } + }} + ></Selector> + ), + accessorKey: "type", + cell: ({ row: { original, index } }) => { + return ( + <Select + value={ + subtitlesTypeOptions.find((s) => { + if (original.hi) { + return s.value === "hi"; + } + + if (original.forced) { + return s.value === "forced"; + } + + return s.value === "normal"; + })?.value + } + data={subtitlesTypeOptions} + onChange={(value) => { + if (value) { + action.mutate(index, { + ...original, + hi: value === "hi", + forced: value === "forced", + }); + } + }} + ></Select> + ); + }, + }, + { id: "action", cell: ({ row: { index } }) => { return ( diff --git a/frontend/src/components/forms/ProfileEditForm.module.scss b/frontend/src/components/forms/ProfileEditForm.module.scss index d98b850ff..3d4a8e177 100644 --- a/frontend/src/components/forms/ProfileEditForm.module.scss +++ b/frontend/src/components/forms/ProfileEditForm.module.scss @@ -3,3 +3,11 @@ padding: 0; } } + +.evenly { + flex-wrap: wrap; + + & > div { + flex: 1; + } +} diff --git a/frontend/src/components/forms/ProfileEditForm.tsx b/frontend/src/components/forms/ProfileEditForm.tsx index 75e2f9df7..267951fcb 100644 --- a/frontend/src/components/forms/ProfileEditForm.tsx +++ b/frontend/src/components/forms/ProfileEditForm.tsx @@ -3,6 +3,7 @@ import { Accordion, Button, Checkbox, + Flex, Select, Stack, Switch, @@ -72,9 +73,16 @@ const ProfileEditForm: FunctionComponent<Props> = ({ (value) => value.length > 0, "Must have a name", ), + tag: FormUtils.validation((value) => { + if (!value) { + return true; + } + + return /^[a-z_0-9-]+$/.test(value); + }, "Only lowercase alphanumeric characters, underscores (_) and hyphens (-) are allowed"), items: FormUtils.validation( (value) => value.length > 0, - "Must contain at lease 1 language", + "Must contain at least 1 language", ), }, }); @@ -265,7 +273,24 @@ const ProfileEditForm: FunctionComponent<Props> = ({ })} > <Stack> - <TextInput label="Name" {...form.getInputProps("name")}></TextInput> + <Flex + direction={{ base: "column", sm: "row" }} + gap="sm" + className={styles.evenly} + > + <TextInput label="Name" {...form.getInputProps("name")}></TextInput> + <TextInput + label="Tag" + {...form.getInputProps("tag")} + onBlur={() => + form.setFieldValue( + "tag", + (prev) => + prev?.toLowerCase().trim().replace(/\s+/g, "_") ?? undefined, + ) + } + ></TextInput> + </Flex> <Accordion multiple chevronPosition="right" @@ -274,7 +299,6 @@ const ProfileEditForm: FunctionComponent<Props> = ({ > <Accordion.Item value="Languages"> <Stack> - {form.errors.items} <SimpleTable columns={columns} data={form.values.items} @@ -282,6 +306,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({ <Button fullWidth onClick={addItem}> Add Language </Button> + <Text c="var(--mantine-color-error)">{form.errors.items}</Text> <Selector clearable label="Cutoff" diff --git a/frontend/src/components/forms/SeriesUploadForm.tsx b/frontend/src/components/forms/SeriesUploadForm.tsx index e4482cab4..9ae6308c9 100644 --- a/frontend/src/components/forms/SeriesUploadForm.tsx +++ b/frontend/src/components/forms/SeriesUploadForm.tsx @@ -1,9 +1,9 @@ -import { FunctionComponent, useEffect, useMemo } from "react"; +import React, { FunctionComponent, useEffect, useMemo } from "react"; import { Button, - Checkbox, Divider, MantineColor, + Select, Stack, Text, } from "@mantine/core"; @@ -17,12 +17,13 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ColumnDef } from "@tanstack/react-table"; -import { isString } from "lodash"; +import { isString, uniqBy } from "lodash"; import { useEpisodesBySeriesId, useEpisodeSubtitleModification, useSubtitleInfos, } from "@/apis/hooks"; +import { subtitlesTypeOptions } from "@/components/forms/uploadFormSelectorTypes"; import { Action, Selector } from "@/components/inputs"; import SimpleTable from "@/components/tables/SimpleTable"; import TextPopover from "@/components/TextPopover"; @@ -100,7 +101,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({ const profile = useLanguageProfileBy(series.profileId); const languages = useProfileItemsToLanguages(profile); const languageOptions = useSelectorOptions( - languages, + uniqBy(languages, "code2"), (v) => v.name, (v) => v.code2, ); @@ -236,42 +237,6 @@ const SeriesUploadForm: FunctionComponent<Props> = ({ }, }, { - header: "Forced", - accessorKey: "forced", - cell: ({ row: { original, index } }) => { - return ( - <Checkbox - checked={original.forced} - onChange={({ currentTarget: { checked } }) => { - action.mutate(index, { - ...original, - forced: checked, - hi: checked ? false : original.hi, - }); - }} - ></Checkbox> - ); - }, - }, - { - header: "HI", - accessorKey: "hi", - cell: ({ row: { original, index } }) => { - return ( - <Checkbox - checked={original.hi} - onChange={({ currentTarget: { checked } }) => { - action.mutate(index, { - ...original, - hi: checked, - forced: checked ? false : original.forced, - }); - }} - ></Checkbox> - ); - }, - }, - { header: () => ( <Selector {...languageOptions} @@ -280,8 +245,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({ onChange={(value) => { if (value) { action.update((item) => { - item.language = value; - return item; + return { ...item, language: value }; }); } }} @@ -302,6 +266,61 @@ const SeriesUploadForm: FunctionComponent<Props> = ({ }, }, { + header: () => ( + <Selector + options={subtitlesTypeOptions} + value={null} + placeholder="Type" + onChange={(value) => { + if (value) { + action.update((item) => { + switch (value) { + case "hi": + return { ...item, hi: true, forced: false }; + case "forced": + return { ...item, hi: false, forced: true }; + case "normal": + return { ...item, hi: false, forced: false }; + default: + return item; + } + }); + } + }} + ></Selector> + ), + accessorKey: "type", + cell: ({ row: { original, index } }) => { + return ( + <Select + value={ + subtitlesTypeOptions.find((s) => { + if (original.hi) { + return s.value === "hi"; + } + + if (original.forced) { + return s.value === "forced"; + } + + return s.value === "normal"; + })?.value + } + data={subtitlesTypeOptions} + onChange={(value) => { + if (value) { + action.mutate(index, { + ...original, + hi: value === "hi", + forced: value === "forced", + }); + } + }} + ></Select> + ); + }, + }, + { id: "episode", header: "Episode", accessorKey: "episode", diff --git a/frontend/src/components/forms/uploadFormSelectorTypes.tsx b/frontend/src/components/forms/uploadFormSelectorTypes.tsx new file mode 100644 index 000000000..168cdddb1 --- /dev/null +++ b/frontend/src/components/forms/uploadFormSelectorTypes.tsx @@ -0,0 +1,16 @@ +import { SelectorOption } from "@/components"; + +export const subtitlesTypeOptions: SelectorOption<string>[] = [ + { + label: "Normal", + value: "normal", + }, + { + label: "Hearing-Impaired", + value: "hi", + }, + { + label: "Forced", + value: "forced", + }, +]; diff --git a/frontend/src/components/inputs/Selector.tsx b/frontend/src/components/inputs/Selector.tsx index 1825d314a..092fd24e7 100644 --- a/frontend/src/components/inputs/Selector.tsx +++ b/frontend/src/components/inputs/Selector.tsx @@ -7,7 +7,7 @@ import { Select, SelectProps, } from "@mantine/core"; -import { isNull, isUndefined, noop } from "lodash"; +import { isNull, isUndefined } from "lodash"; import { LOG } from "@/utilities/console"; export type SelectorOption<T> = Override< @@ -49,10 +49,7 @@ export type GroupedSelectorProps<T> = Override< >; export function GroupedSelector<T>({ - value, options, - getkey = DefaultKeyBuilder, - onOptionSubmit = noop, ...select }: GroupedSelectorProps<T>) { return ( |