Skip to content

Commit 0e173f4

Browse files
committed
feat: port translate_from UI (PR morpheus65535#3335) and score/provider/status columns (PR morpheus65535#3337) to LavX
- Add translate_from field to Language.ProfileItem type - Add TranslateFromCell selector in ProfileEditForm - Add min_source_score Number control in Translator settings - Add history prop + Score/Provider/Status columns to Movies/Details/table - Preserve all LavX-specific SubtitleToolsMenu and navigation code
1 parent 1d1bf6a commit 0e173f4

5 files changed

Lines changed: 175 additions & 4 deletions

File tree

frontend/src/components/forms/ProfileEditForm.tsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ const defaultCutoffOptions: SelectorOption<Language.ProfileItem>[] = [
3535
forced: "False",
3636
hi: "False",
3737
language: "any",
38+
// eslint-disable-next-line camelcase
39+
translate_from: null,
3840
},
3941
},
4042
];
@@ -165,6 +167,8 @@ const ProfileEditForm: FunctionComponent<Props> = ({
165167
audio_only_include: "False",
166168
hi: "False",
167169
forced: "False",
170+
// eslint-disable-next-line camelcase
171+
translate_from: null,
168172
};
169173

170174
const list = [...form.values.items, item];
@@ -260,6 +264,48 @@ const ProfileEditForm: FunctionComponent<Props> = ({
260264
},
261265
);
262266

267+
const TranslateFromCell = React.memo(
268+
({ item, index }: { item: Language.ProfileItem; index: number }) => {
269+
// Exclude the item's own language from the source list (can't translate
270+
// a language to itself). Treat undefined translate_from as null for
271+
// backward compatibility with profile rows persisted before this field.
272+
const translateFromValue = item.translate_from ?? null;
273+
274+
const filteredOptions = useMemo(
275+
() =>
276+
languageOptions.options.filter(
277+
(l) => l.value.code2 !== item.language,
278+
),
279+
[item.language],
280+
);
281+
282+
const selected = useMemo(
283+
() =>
284+
filteredOptions.find((l) => l.value.code2 === translateFromValue)
285+
?.value ?? null,
286+
[filteredOptions, translateFromValue],
287+
);
288+
289+
return (
290+
<Selector
291+
{...languageOptions}
292+
options={filteredOptions}
293+
className="table-select"
294+
clearable
295+
placeholder="None"
296+
value={selected}
297+
onChange={(value) => {
298+
action.mutate(index, {
299+
...item,
300+
// eslint-disable-next-line camelcase
301+
translate_from: value?.code2 ?? null,
302+
});
303+
}}
304+
></Selector>
305+
);
306+
},
307+
);
308+
263309
const columns = useMemo<ColumnDef<Language.ProfileItem>[]>(
264310
() => [
265311
{
@@ -287,6 +333,13 @@ const ProfileEditForm: FunctionComponent<Props> = ({
287333
return <InclusionCell item={item} index={index} />;
288334
},
289335
},
336+
{
337+
header: "Translate From",
338+
accessorKey: "translate_from",
339+
cell: ({ row: { original: item, index } }) => {
340+
return <TranslateFromCell item={item} index={index} />;
341+
},
342+
},
290343
{
291344
id: "action",
292345
cell: ({ row }) => {
@@ -301,7 +354,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
301354
},
302355
},
303356
],
304-
[action, LanguageCell, SubtitleTypeCell, InclusionCell],
357+
[action, LanguageCell, SubtitleTypeCell, InclusionCell, TranslateFromCell],
305358
);
306359

307360
return (

frontend/src/pages/Movies/Details/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
import {
3434
useMovieAction,
3535
useMovieById,
36+
useMovieHistory,
3637
useMovieModification,
3738
} from "@/apis/hooks/movies";
3839
import { useInstanceName } from "@/apis/hooks/site";
@@ -54,6 +55,7 @@ const MovieDetailView: FunctionComponent = () => {
5455
const id = Number.parseInt(param.id ?? "");
5556
const movieQuery = useMovieById(id);
5657
const { data: movie, isFetched } = movieQuery;
58+
const { data: movieHistory } = useMovieHistory(movie?.radarrId);
5759

5860
const profile = useLanguageProfileBy(movie?.profileId);
5961

@@ -272,6 +274,7 @@ const MovieDetailView: FunctionComponent = () => {
272274
movie={movie ?? null}
273275
profile={profile}
274276
disabled={hasTask}
277+
history={movieHistory}
275278
></Table>
276279
</Stack>
277280
</QueryOverlay>

frontend/src/pages/Movies/Details/table.tsx

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import React, { FunctionComponent, useMemo } from "react";
22
import { useNavigate } from "react-router";
3-
import { Badge, Text, TextProps } from "@mantine/core";
3+
import { Badge, Group, Text, TextProps } from "@mantine/core";
44
import { faEllipsis } from "@fortawesome/free-solid-svg-icons";
55
import { ColumnDef } from "@tanstack/react-table";
66
import { isString } from "lodash";
77
import { useMovieSubtitleModification } from "@/apis/hooks";
88
import { useShowOnlyDesired } from "@/apis/hooks/site";
99
import { Action } from "@/components";
10+
import { HistoryIcon } from "@/components/bazarr";
1011
import Language from "@/components/bazarr/Language";
1112
import SubtitleToolsMenu from "@/components/SubtitleToolsMenu";
1213
import SimpleTable from "@/components/tables/SimpleTable";
@@ -19,8 +20,28 @@ interface Props {
1920
movie: Item.Movie | null;
2021
disabled?: boolean;
2122
profile?: Language.Profile;
23+
history?: History.Movie[];
2224
}
2325

26+
const ScoreBadge: React.FC<{ score?: string | number | null }> = ({
27+
score,
28+
}) => {
29+
if (score === undefined || score === null || score === "") {
30+
return (
31+
<Text c="dimmed" size="xs">
32+
33+
</Text>
34+
);
35+
}
36+
const pct = typeof score === "string" ? parseFloat(score) : score;
37+
const color = pct >= 90 ? "green" : pct >= 70 ? "yellow" : "red";
38+
return (
39+
<Badge color={color} size="sm" variant="light">
40+
{typeof score === "number" ? `${score}%` : score}
41+
</Badge>
42+
);
43+
};
44+
2445
function isSubtitleTrack(path: string | undefined | null) {
2546
return !isString(path) || path.length === 0;
2647
}
@@ -36,7 +57,7 @@ function buildLanguageKey(sub: Subtitle): string {
3657
return key;
3758
}
3859

39-
const Table: FunctionComponent<Props> = ({ movie, profile }) => {
60+
const Table: FunctionComponent<Props> = ({ movie, profile, history }) => {
4061
const onlyDesired = useShowOnlyDesired();
4162

4263
const profileItems = useProfileItemsToLanguages(profile);
@@ -52,6 +73,29 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
5273
[movie?.subtitles],
5374
);
5475

76+
const historyMap = useMemo(() => {
77+
const map = new Map<string, History.Movie>();
78+
history?.forEach((h) => {
79+
if (!h.subtitles_path) return;
80+
if ([1, 2, 3].includes(h.action)) {
81+
if (!map.has(h.subtitles_path)) map.set(h.subtitles_path, h);
82+
}
83+
});
84+
return map;
85+
}, [history]);
86+
87+
const statusMap = useMemo(() => {
88+
const map = new Map<string, Set<number>>();
89+
history?.forEach((h) => {
90+
if (!h.subtitles_path) return;
91+
if ([5, 6].includes(h.action)) {
92+
if (!map.has(h.subtitles_path)) map.set(h.subtitles_path, new Set());
93+
map.get(h.subtitles_path)!.add(h.action);
94+
}
95+
});
96+
return map;
97+
}, [history]);
98+
5599
const navigate = useNavigate();
56100

57101
const CodeCell = React.memo(({ item }: { item: Subtitle }) => {
@@ -192,14 +236,56 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
192236
}
193237
},
194238
},
239+
{
240+
id: "score",
241+
header: "Score",
242+
cell: ({ row: { original } }) => {
243+
const record = historyMap.get(original.path ?? "");
244+
return <ScoreBadge score={record?.score} />;
245+
},
246+
},
247+
{
248+
id: "provider",
249+
header: "Provider",
250+
cell: ({ row: { original } }) => {
251+
const record = historyMap.get(original.path ?? "");
252+
if (!record?.provider)
253+
return (
254+
<Text c="dimmed" size="xs">
255+
256+
</Text>
257+
);
258+
return <Text size="xs">{record.provider}</Text>;
259+
},
260+
},
261+
{
262+
id: "status",
263+
header: "Status",
264+
cell: ({ row: { original } }) => {
265+
const actions = statusMap.get(original.path ?? "");
266+
if (!actions?.size)
267+
return (
268+
<Text c="dimmed" size="xs">
269+
270+
</Text>
271+
);
272+
return (
273+
<Group gap={4}>
274+
{Array.from(actions).map((action) => (
275+
<HistoryIcon key={action} action={action} />
276+
))}
277+
</Group>
278+
);
279+
},
280+
},
195281
{
196282
id: "code2",
197283
cell: ({ row: { original } }) => {
198284
return <CodeCell item={original} />;
199285
},
200286
},
201287
],
202-
[CodeCell],
288+
[CodeCell, historyMap, statusMap],
203289
);
204290

205291
const data: Subtitle[] = useMemo(() => {

frontend/src/pages/Settings/Translator/index.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,34 @@ const SettingsTranslatorView: FunctionComponent = () => {
291291
</MantineText>
292292
</Tooltip>
293293
</Group>
294+
<Group gap="xs" align="center">
295+
<MantineText size="sm" c="var(--bz-text-tertiary)">
296+
Min Source Score
297+
</MantineText>
298+
<Number
299+
settingKey="settings-translator-min_source_score"
300+
min={0}
301+
max={100}
302+
step={1}
303+
w={70}
304+
size="xs"
305+
/>
306+
<Tooltip
307+
label="Minimum quality score (0-100) a source subtitle must reach before auto-translation triggers via a language profile's 'Translate From' setting. Lower-scoring sources are likely badly synced or poorly matched."
308+
multiline
309+
w={280}
310+
withArrow
311+
>
312+
<MantineText
313+
size="xs"
314+
c="var(--bz-text-tertiary)"
315+
style={{ cursor: "help" }}
316+
component="span"
317+
>
318+
<FontAwesomeIcon icon={faCircleInfo} />
319+
</MantineText>
320+
</Tooltip>
321+
</Group>
294322
<Group gap="xs" align="center">
295323
<Check
296324
label="Translation credit"

frontend/src/types/api.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ declare namespace Language {
3131
forced: PythonBoolean;
3232
hi: PythonBoolean;
3333
language: CodeType;
34+
translate_from: CodeType | null;
3435
}
3536

3637
interface Profile {

0 commit comments

Comments
 (0)