mirror of
https://github.com/stashapp/stash.git
synced 2026-02-10 01:12:06 +01:00
duplicate phash
This commit is contained in:
parent
c6ae43c1d6
commit
f4733f4e8d
4 changed files with 144 additions and 0 deletions
118
ui/v2.5/src/components/List/Filters/DuplicateFilter.tsx
Normal file
118
ui/v2.5/src/components/List/Filters/DuplicateFilter.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import React, { useMemo, useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { BooleanCriterion } from "src/models/list-filter/criteria/criterion";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { Option, SidebarListFilter } from "./SidebarListFilter";
|
||||
import { DuplicatedCriterionOption } from "src/models/list-filter/criteria/phash";
|
||||
|
||||
interface ISidebarDuplicateFilterProps {
|
||||
title?: React.ReactNode;
|
||||
filter: ListFilterModel;
|
||||
setFilter: (f: ListFilterModel) => void;
|
||||
sectionID?: string;
|
||||
}
|
||||
|
||||
export const SidebarDuplicateFilter: React.FC<ISidebarDuplicateFilterProps> = ({
|
||||
title,
|
||||
filter,
|
||||
setFilter,
|
||||
sectionID,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [expandedType, setExpandedType] = useState<string | null>(null);
|
||||
|
||||
const trueLabel = intl.formatMessage({ id: "true" });
|
||||
const falseLabel = intl.formatMessage({ id: "false" });
|
||||
const phashLabel = intl.formatMessage({ id: "media_info.phash" });
|
||||
|
||||
const criteria = filter.criteriaFor(
|
||||
DuplicatedCriterionOption.type
|
||||
) as BooleanCriterion[];
|
||||
const criterion = criteria.length > 0 ? criteria[0] : null;
|
||||
|
||||
// The main duplicate type option
|
||||
const phashOption = useMemo(
|
||||
() => ({
|
||||
id: "phash",
|
||||
label: phashLabel,
|
||||
}),
|
||||
[phashLabel]
|
||||
);
|
||||
|
||||
// Determine if pHash is selected (has a true/false value)
|
||||
const phashSelected = criterion !== null;
|
||||
|
||||
// Selected shows "pHash: True" or "pHash: False" when a value is set
|
||||
const selected: Option[] = useMemo(() => {
|
||||
if (!criterion) return [];
|
||||
|
||||
const valueLabel = criterion.value === "true" ? trueLabel : falseLabel;
|
||||
return [
|
||||
{
|
||||
id: "phash",
|
||||
label: `${phashLabel}: ${valueLabel}`,
|
||||
},
|
||||
];
|
||||
}, [criterion, phashLabel, trueLabel, falseLabel]);
|
||||
|
||||
// Available options - show pHash if not selected
|
||||
const options: Option[] = useMemo(() => {
|
||||
if (phashSelected) return [];
|
||||
return [phashOption];
|
||||
}, [phashSelected, phashOption]);
|
||||
|
||||
function onSelect(item: Option) {
|
||||
if (item.id === "phash") {
|
||||
// Expand to show True/False options
|
||||
setExpandedType("phash");
|
||||
}
|
||||
}
|
||||
|
||||
function onUnselect() {
|
||||
setFilter(filter.removeCriterion(DuplicatedCriterionOption.type));
|
||||
setExpandedType(null);
|
||||
}
|
||||
|
||||
function onSelectValue(value: "true" | "false") {
|
||||
const newCriterion = criterion
|
||||
? criterion.clone()
|
||||
: DuplicatedCriterionOption.makeCriterion();
|
||||
newCriterion.value = value;
|
||||
setFilter(
|
||||
filter.replaceCriteria(DuplicatedCriterionOption.type, [newCriterion])
|
||||
);
|
||||
setExpandedType(null);
|
||||
}
|
||||
|
||||
// Sub-options shown when pHash is clicked
|
||||
const subOptions =
|
||||
expandedType === "phash" ? (
|
||||
<div className="duplicate-sub-options">
|
||||
<div
|
||||
className="duplicate-sub-option"
|
||||
onClick={() => onSelectValue("true")}
|
||||
>
|
||||
{trueLabel}
|
||||
</div>
|
||||
<div
|
||||
className="duplicate-sub-option"
|
||||
onClick={() => onSelectValue("false")}
|
||||
>
|
||||
{falseLabel}
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<SidebarListFilter
|
||||
title={title}
|
||||
candidates={options}
|
||||
onSelect={onSelect}
|
||||
onUnselect={onUnselect}
|
||||
selected={selected}
|
||||
singleValue
|
||||
postCandidates={subOptions}
|
||||
sectionID={sectionID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -726,6 +726,24 @@ input[type="range"].zoom-slider {
|
|||
min-height: 2em;
|
||||
}
|
||||
|
||||
.duplicate-sub-options {
|
||||
margin-left: 2rem;
|
||||
padding-left: 0.5rem;
|
||||
|
||||
.duplicate-sub-option {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 2em;
|
||||
opacity: 0.8;
|
||||
padding-left: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(138, 155, 168, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tilted {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import {
|
|||
DurationCriterionOption,
|
||||
PerformerAgeCriterionOption,
|
||||
} from "src/models/list-filter/scenes";
|
||||
import { SidebarDuplicateFilter } from "../List/Filters/DuplicateFilter";
|
||||
import { SidebarAgeFilter } from "../List/Filters/SidebarAgeFilter";
|
||||
import { SidebarDurationFilter } from "../List/Filters/SidebarDurationFilter";
|
||||
import {
|
||||
|
|
@ -351,6 +352,12 @@ const SidebarContent: React.FC<{
|
|||
setFilter={setFilter}
|
||||
sectionID="organized"
|
||||
/>
|
||||
<SidebarDuplicateFilter
|
||||
title={<FormattedMessage id="duplicated" />}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
sectionID="duplicated"
|
||||
/>
|
||||
<SidebarAgeFilter
|
||||
title={<FormattedMessage id="performer_age" />}
|
||||
option={PerformerAgeCriterionOption}
|
||||
|
|
|
|||
|
|
@ -1059,6 +1059,7 @@
|
|||
"select_youngest": "Select the youngest file in the duplicate group",
|
||||
"title": "Duplicate Scenes"
|
||||
},
|
||||
"duplicated": "Duplicated",
|
||||
"duplicated_phash": "Duplicated (pHash)",
|
||||
"duration": "Duration",
|
||||
"effect_filters": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue