aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/src/Router/index.tsx2
-rw-r--r--frontend/src/components/Search.tsx41
-rw-r--r--frontend/src/components/forms/MovieUploadForm.tsx92
-rw-r--r--frontend/src/components/forms/SeriesUploadForm.tsx103
-rw-r--r--frontend/src/components/forms/uploadFormSelectorTypes.tsx16
-rw-r--r--frontend/src/pages/Movies/index.tsx3
-rw-r--r--frontend/src/pages/Settings/Languages/table.tsx44
-rw-r--r--frontend/src/pages/System/Status/index.tsx2
-rw-r--r--frontend/src/types/system.d.ts2
9 files changed, 217 insertions, 88 deletions
diff --git a/frontend/src/Router/index.tsx b/frontend/src/Router/index.tsx
index d600fc87d..8ccea87f9 100644
--- a/frontend/src/Router/index.tsx
+++ b/frontend/src/Router/index.tsx
@@ -270,6 +270,7 @@ function useRoutes(): CustomRouteObject[] {
{
path: "status",
name: "Status",
+ badge: data?.status,
element: (
<Lazy>
<SystemStatusView></SystemStatusView>
@@ -309,6 +310,7 @@ function useRoutes(): CustomRouteObject[] {
data?.sonarr_signalr,
data?.radarr_signalr,
data?.announcements,
+ data?.status,
radarr,
sonarr,
],
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/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/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/pages/Movies/index.tsx b/frontend/src/pages/Movies/index.tsx
index 0429e1fdd..ef5a1ec0d 100644
--- a/frontend/src/pages/Movies/index.tsx
+++ b/frontend/src/pages/Movies/index.tsx
@@ -6,6 +6,7 @@ import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
+import { uniqueId } from "lodash";
import { useMovieModification, useMoviesPagination } from "@/apis/hooks";
import { Action } from "@/components";
import { AudioList } from "@/components/bazarr";
@@ -95,7 +96,7 @@ const MovieView: FunctionComponent = () => {
<Badge
mr="xs"
color="yellow"
- key={BuildKey(v.code2, v.hi, v.forced)}
+ key={uniqueId(`${BuildKey(v.code2, v.hi, v.forced)}_`)}
>
<Language.Text value={v}></Language.Text>
</Badge>
diff --git a/frontend/src/pages/Settings/Languages/table.tsx b/frontend/src/pages/Settings/Languages/table.tsx
index 03971a5cc..5cfefdfa9 100644
--- a/frontend/src/pages/Settings/Languages/table.tsx
+++ b/frontend/src/pages/Settings/Languages/table.tsx
@@ -2,7 +2,7 @@ import { FunctionComponent, useCallback, useMemo } from "react";
import { Badge, Button, Group } from "@mantine/core";
import { faTrash, faWrench } from "@fortawesome/free-solid-svg-icons";
import { ColumnDef } from "@tanstack/react-table";
-import { cloneDeep } from "lodash";
+import { cloneDeep, includes, maxBy } from "lodash";
import { Action } from "@/components";
import {
anyCutoff,
@@ -79,10 +79,10 @@ const Table: FunctionComponent = () => {
}) => {
return (
<Group gap="xs" wrap="nowrap">
- {items.map((v) => {
+ {items.map((v, i) => {
const isCutoff = v.id === cutoff || cutoff === anyCutoff;
return (
- <ItemBadge key={v.id} cutoff={isCutoff} item={v}></ItemBadge>
+ <ItemBadge key={i} cutoff={isCutoff} item={v}></ItemBadge>
);
})}
</Group>
@@ -148,9 +148,45 @@ const Table: FunctionComponent = () => {
icon={faWrench}
c="gray"
onClick={() => {
+ const lastId = maxBy(profile.items, "id")?.id || 0;
+
+ // We once had an issue on the past where there were duplicated
+ // item ids that needs to become unique upon editing.
+ const sanitizedProfile = {
+ ...cloneDeep(profile),
+ items: profile.items.reduce(
+ (acc, value) => {
+ const { ids, duplicatedIds, items } = acc;
+
+ // We once had an issue on the past where there were duplicated
+ // item ids that needs to become unique upon editing.
+ if (includes(ids, value.id)) {
+ duplicatedIds.push(value.id);
+ items.push({
+ ...value,
+ id: lastId + duplicatedIds.length,
+ });
+
+ return acc;
+ }
+
+ ids.push(value.id);
+ items.push(value);
+
+ return acc;
+ },
+ {
+ ids: [] as number[],
+ duplicatedIds: [] as number[],
+ items: [] as typeof profile.items,
+ },
+ ).items,
+ tag: profile.tag || undefined,
+ };
+
modals.openContextModal(ProfileEditModal, {
languages,
- profile: cloneDeep(profile),
+ profile: sanitizedProfile,
onComplete: updateProfile,
});
}}
diff --git a/frontend/src/pages/System/Status/index.tsx b/frontend/src/pages/System/Status/index.tsx
index bcd0e175d..157935dfb 100644
--- a/frontend/src/pages/System/Status/index.tsx
+++ b/frontend/src/pages/System/Status/index.tsx
@@ -144,6 +144,8 @@ const SystemStatusView: FunctionComponent = () => {
<Row title="Radarr Version">{status?.radarr_version}</Row>
<Row title="Operating System">{status?.operating_system}</Row>
<Row title="Python Version">{status?.python_version}</Row>
+ <Row title="Database Engine">{status?.database_engine}</Row>
+ <Row title="Database Version">{status?.database_migration}</Row>
<Row title="Bazarr Directory">{status?.bazarr_directory}</Row>
<Row title="Bazarr Config Directory">
{status?.bazarr_config_directory}
diff --git a/frontend/src/types/system.d.ts b/frontend/src/types/system.d.ts
index 544d969ae..5a477fb54 100644
--- a/frontend/src/types/system.d.ts
+++ b/frontend/src/types/system.d.ts
@@ -20,6 +20,8 @@ declare namespace System {
bazarr_config_directory: string;
bazarr_directory: string;
bazarr_version: string;
+ database_engine: string;
+ database_migration: string;
operating_system: string;
package_version: string;
python_version: string;