feat(filters): the any/all switch sits on the panel above its picker
ui/MatchToggle replaces the Combobox popover footer (Feed tags, topics, languages; Plex collections) and PlexSidebar's own toggle (genres, actors): shown from two picks, one wording. A closed picker had hidden that "all" was on.
This commit is contained in:
@@ -12,7 +12,7 @@ import { type MediaSourceKind } from "../lib/mediaSource";
|
|||||||
import { useDebounced } from "../lib/useDebounced";
|
import { useDebounced } from "../lib/useDebounced";
|
||||||
import FilterRail from "./FilterRail";
|
import FilterRail from "./FilterRail";
|
||||||
import { type PanelItem } from "./ui/PanelGroups";
|
import { type PanelItem } from "./ui/PanelGroups";
|
||||||
import SegmentedToggle from "./ui/SegmentedToggle";
|
import MatchToggle from "./ui/MatchToggle";
|
||||||
import FilterChipGroup from "./ui/FilterChipGroup";
|
import FilterChipGroup from "./ui/FilterChipGroup";
|
||||||
import Combobox, { type ComboboxOption } from "./ui/Combobox";
|
import Combobox, { type ComboboxOption } from "./ui/Combobox";
|
||||||
import SavedViewsPopover from "./SavedViewsPopover";
|
import SavedViewsPopover from "./SavedViewsPopover";
|
||||||
@@ -43,31 +43,6 @@ const DURATION_BUCKETS: { key: string; min: number | null; max: number | null }[
|
|||||||
{ key: "long", min: 120 * 60, max: null },
|
{ key: "long", min: 120 * 60, max: null },
|
||||||
];
|
];
|
||||||
|
|
||||||
// The any/all combine toggle for a multi-value filter (genres, actors) — a thin binding of the shared
|
|
||||||
// SegmentedToggle to the Plex match vocabulary (same look as the feed rail's tag MATCH toggle).
|
|
||||||
function MatchToggle({
|
|
||||||
mode,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
mode: "any" | "all";
|
|
||||||
onChange: (m: "any" | "all") => void;
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<SegmentedToggle
|
|
||||||
size="sm"
|
|
||||||
label={t("plex.filter.matchLabel")}
|
|
||||||
ariaLabel={t("plex.filter.matchLabel")}
|
|
||||||
options={[
|
|
||||||
{ value: "any", label: t("plex.filter.match.any") },
|
|
||||||
{ value: "all", label: t("plex.filter.match.all") },
|
|
||||||
]}
|
|
||||||
value={mode}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PlexSidebar({ layout, setLayout, collapsed, onToggleCollapse }: Props) {
|
export default function PlexSidebar({ layout, setLayout, collapsed, onToggleCollapse }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// Scope / watch-state / sort / expanded filters live in PlexProvider (shared with PlexBrowse and
|
// Scope / watch-state / sort / expanded filters live in PlexProvider (shared with PlexBrowse and
|
||||||
@@ -131,14 +106,12 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
|
|||||||
onRemove={() => patch({ directors: filters.directors.filter((x) => x !== d) })}
|
onRemove={() => patch({ directors: filters.directors.filter((x) => x !== d) })}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{filters.actors.length > 1 && (
|
<MatchToggle
|
||||||
<div className="w-full">
|
picked={filters.actors.length}
|
||||||
<MatchToggle
|
className="w-full"
|
||||||
mode={filters.actorMode ?? "any"}
|
value={filters.actorMode ?? "any"}
|
||||||
onChange={(m) => patch({ actorMode: m })}
|
onChange={(m) => patch({ actorMode: m })}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{filters.actors.map((a) => (
|
{filters.actors.map((a) => (
|
||||||
<RemovableChip
|
<RemovableChip
|
||||||
key={`a:${a}`}
|
key={`a:${a}`}
|
||||||
@@ -186,14 +159,11 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
|
|||||||
title: t("plex.filter.genre"),
|
title: t("plex.filter.genre"),
|
||||||
body: (
|
body: (
|
||||||
<>
|
<>
|
||||||
{filters.genres.length > 1 && (
|
<MatchToggle
|
||||||
<div className="mb-1.5">
|
picked={filters.genres.length}
|
||||||
<MatchToggle
|
value={filters.genreMode ?? "any"}
|
||||||
mode={filters.genreMode ?? "any"}
|
onChange={(m) => patch({ genreMode: m })}
|
||||||
onChange={(m) => patch({ genreMode: m })}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Combobox
|
<Combobox
|
||||||
label={t("plex.filter.genre")}
|
label={t("plex.filter.genre")}
|
||||||
options={facets.genres}
|
options={facets.genres}
|
||||||
@@ -472,29 +442,30 @@ function CollectionPicker({
|
|||||||
for (const o of options) if (o.label) labelMap[o.value] = o.label;
|
for (const o of options) if (o.label) labelMap[o.value] = o.label;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<>
|
||||||
label={t("plex.filter.collection")}
|
<MatchToggle picked={values.length} value={mode} onChange={onModeChange} />
|
||||||
clientFilter={false} // the server already narrows to `search`
|
<Combobox
|
||||||
options={options}
|
label={t("plex.filter.collection")}
|
||||||
selected={values}
|
clientFilter={false} // the server already narrows to `search`
|
||||||
selectedLabels={resolved}
|
options={options}
|
||||||
query={search}
|
selected={values}
|
||||||
onQueryChange={setSearch}
|
selectedLabels={resolved}
|
||||||
onOpenChange={setOpen}
|
query={search}
|
||||||
loading={q.isFetching}
|
onQueryChange={setSearch}
|
||||||
placeholder={t("plex.filter.collectionSearch")}
|
onOpenChange={setOpen}
|
||||||
emptyText={t("plex.filter.collectionEmpty")}
|
loading={q.isFetching}
|
||||||
matchMode={mode === "all" ? "and" : "or"}
|
placeholder={t("plex.filter.collectionSearch")}
|
||||||
onMatchModeChange={(m) => onModeChange(m === "and" ? "all" : "any")}
|
emptyText={t("plex.filter.collectionEmpty")}
|
||||||
onChange={(next) => {
|
onChange={(next) => {
|
||||||
// Carry only the selected ids' titles forward (drop titles for de-selected ones). Collapse an
|
// Carry only the selected ids' titles forward (drop titles for de-selected ones). Collapse an
|
||||||
// empty map to undefined so a clear-all key-matches the pristine state (an empty {} still hashes
|
// empty map to undefined so a clear-all key-matches the pristine state (an empty {} still hashes
|
||||||
// differently from an absent key), mirroring the chip-row remove handler.
|
// differently from an absent key), mirroring the chip-row remove handler.
|
||||||
const nextTitles: Record<string, string> = {};
|
const nextTitles: Record<string, string> = {};
|
||||||
for (const id of next) nextTitles[id] = labelMap[id] ?? titles?.[id] ?? id;
|
for (const id of next) nextTitles[id] = labelMap[id] ?? titles?.[id] ?? id;
|
||||||
onChange(next, next.length ? nextTitles : undefined);
|
onChange(next, next.length ? nextTitles : undefined);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import ChannelPicker from "./ChannelPicker";
|
|||||||
import DateFilterSelect from "./DateFilterSelect";
|
import DateFilterSelect from "./DateFilterSelect";
|
||||||
import YearFilter from "./YearFilter";
|
import YearFilter from "./YearFilter";
|
||||||
import Combobox, { type ComboboxOption } from "./ui/Combobox";
|
import Combobox, { type ComboboxOption } from "./ui/Combobox";
|
||||||
|
import MatchToggle from "./ui/MatchToggle";
|
||||||
import { useFeedFilters, useFeedFiltersActions } from "./feedFiltersContext";
|
import { useFeedFilters, useFeedFiltersActions } from "./feedFiltersContext";
|
||||||
import { useChannelsActions } from "./channelsContext";
|
import { useChannelsActions } from "./channelsContext";
|
||||||
import { useMe } from "../lib/useMe";
|
import { useMe } from "../lib/useMe";
|
||||||
@@ -101,7 +102,8 @@ export default function Sidebar({
|
|||||||
tags: [...filters.tags.filter((id) => !inList.has(id)), ...next.map(Number)],
|
tags: [...filters.tags.filter((id) => !inList.has(id)), ...next.map(Number)],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Each tag category (language/topic/other) has its OWN any/all match, set from its combobox footer.
|
// Each tag category (language/topic/other) has its OWN any/all match, set from the switch above its
|
||||||
|
// picker (ui/MatchToggle); the store keeps the older or/and words.
|
||||||
const setTagMode = (category: TagCategory, mode: "or" | "and") =>
|
const setTagMode = (category: TagCategory, mode: "or" | "and") =>
|
||||||
setFilters({ ...filters, tagModes: { ...filters.tagModes, [category]: mode } });
|
setFilters({ ...filters, tagModes: { ...filters.tagModes, [category]: mode } });
|
||||||
|
|
||||||
@@ -125,18 +127,23 @@ export default function Sidebar({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<>
|
||||||
label={t("sidebar.widget.tags")}
|
<MatchToggle
|
||||||
options={[
|
picked={selectedIn(userTags).length}
|
||||||
{ value: NO_TAG_SENTINEL, label: t("sidebar.noTagOption") },
|
value={filters.tagModes.other === "and" ? "all" : "any"}
|
||||||
...visibleChips(userTags).map(tagOption),
|
onChange={(m) => setTagMode("other", m === "all" ? "and" : "or")}
|
||||||
]}
|
/>
|
||||||
selected={noneOn ? [NO_TAG_SENTINEL] : selectedIn(userTags)}
|
<Combobox
|
||||||
onChange={onChange}
|
label={t("sidebar.widget.tags")}
|
||||||
emptyText={t("sidebar.noMatchingTags")}
|
options={[
|
||||||
matchMode={filters.tagModes.other}
|
{ value: NO_TAG_SENTINEL, label: t("sidebar.noTagOption") },
|
||||||
onMatchModeChange={(m) => setTagMode("other", m)}
|
...visibleChips(userTags).map(tagOption),
|
||||||
/>
|
]}
|
||||||
|
selected={noneOn ? [NO_TAG_SENTINEL] : selectedIn(userTags)}
|
||||||
|
onChange={onChange}
|
||||||
|
emptyText={t("sidebar.noMatchingTags")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,27 +172,37 @@ export default function Sidebar({
|
|||||||
return <YearFilter filters={filters} setFilters={setFilters} />;
|
return <YearFilter filters={filters} setFilters={setFilters} />;
|
||||||
case "language":
|
case "language":
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<>
|
||||||
label={t("sidebar.widget.language")}
|
<MatchToggle
|
||||||
options={visibleChips(languages).map(tagOption)}
|
picked={selectedIn(languages).length}
|
||||||
selected={selectedIn(languages)}
|
value={filters.tagModes.language === "and" ? "all" : "any"}
|
||||||
onChange={(next) => setTagSelection(languages, next)}
|
onChange={(m) => setTagMode("language", m === "all" ? "and" : "or")}
|
||||||
emptyText={t("sidebar.noMatchingTags")}
|
/>
|
||||||
matchMode={filters.tagModes.language}
|
<Combobox
|
||||||
onMatchModeChange={(m) => setTagMode("language", m)}
|
label={t("sidebar.widget.language")}
|
||||||
/>
|
options={visibleChips(languages).map(tagOption)}
|
||||||
|
selected={selectedIn(languages)}
|
||||||
|
onChange={(next) => setTagSelection(languages, next)}
|
||||||
|
emptyText={t("sidebar.noMatchingTags")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
case "topic":
|
case "topic":
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<>
|
||||||
label={t("sidebar.widget.topic")}
|
<MatchToggle
|
||||||
options={visibleChips(topics).map(tagOption)}
|
picked={selectedIn(topics).length}
|
||||||
selected={selectedIn(topics)}
|
value={filters.tagModes.topic === "and" ? "all" : "any"}
|
||||||
onChange={(next) => setTagSelection(topics, next)}
|
onChange={(m) => setTagMode("topic", m === "all" ? "and" : "or")}
|
||||||
emptyText={t("sidebar.noMatchingTags")}
|
/>
|
||||||
matchMode={filters.tagModes.topic}
|
<Combobox
|
||||||
onMatchModeChange={(m) => setTagMode("topic", m)}
|
label={t("sidebar.widget.topic")}
|
||||||
/>
|
options={visibleChips(topics).map(tagOption)}
|
||||||
|
selected={selectedIn(topics)}
|
||||||
|
onChange={(next) => setTagSelection(topics, next)}
|
||||||
|
emptyText={t("sidebar.noMatchingTags")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
case "tags":
|
case "tags":
|
||||||
// The widget's own "Manage" trigger is its title (see the PanelItem onTitleClick below), so the
|
// The widget's own "Manage" trigger is its title (see the PanelItem onTitleClick below), so the
|
||||||
|
|||||||
@@ -205,31 +205,6 @@ describe("Combobox", () => {
|
|||||||
expect(onChange).toHaveBeenCalledWith(["id1"]); // single-select replaces
|
expect(onChange).toHaveBeenCalledWith(["id1"]); // single-select replaces
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows the any/all match footer only at 2+ selections and reports a mode change", () => {
|
|
||||||
// The per-category any/all toggle: meaningless below 2 selections (any === all), so it appears
|
|
||||||
// only at >= 2, and clicking 'all' reports the mode change out.
|
|
||||||
const onMatchModeChange = vi.fn();
|
|
||||||
const props = {
|
|
||||||
label: "Topic",
|
|
||||||
options: OPTIONS,
|
|
||||||
onChange: vi.fn(),
|
|
||||||
matchMode: "or" as const,
|
|
||||||
onMatchModeChange,
|
|
||||||
};
|
|
||||||
// One selection: no footer.
|
|
||||||
const { rerender } = wrap(<Combobox {...props} selected={["Drama"]} />);
|
|
||||||
fireEvent.focus(screen.getByRole("combobox"));
|
|
||||||
expect(screen.queryByText("sidebar.all")).toBeNull();
|
|
||||||
|
|
||||||
// Two selections: footer appears with an any/all segment.
|
|
||||||
rerender(<FloatingTree>{<Combobox {...props} selected={["Drama", "Action"]} />}</FloatingTree>);
|
|
||||||
fireEvent.focus(screen.getByRole("combobox"));
|
|
||||||
const all = screen.getByText("sidebar.all");
|
|
||||||
expect(all).toBeTruthy();
|
|
||||||
fireEvent.click(all);
|
|
||||||
expect(onMatchModeChange).toHaveBeenCalledWith("and");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("single-select: picking replaces the value and closes the list", () => {
|
it("single-select: picking replaces the value and closes the list", () => {
|
||||||
const onChange = vi.fn();
|
const onChange = vi.fn();
|
||||||
wrap(
|
wrap(
|
||||||
|
|||||||
@@ -61,11 +61,6 @@ export interface ComboboxProps {
|
|||||||
/** Chip/label text for a selected value whose option is not in the current (e.g. server-narrowed)
|
/** Chip/label text for a selected value whose option is not in the current (e.g. server-narrowed)
|
||||||
* list — the collection picker passes the stored title here so a reloaded filter still names itself. */
|
* list — the collection picker passes the stored title here so a reloaded filter still names itself. */
|
||||||
selectedLabels?: Record<string, string>;
|
selectedLabels?: Record<string, string>;
|
||||||
/** Optional "any/all" match segment in the popover footer (like SortControl's direction segment):
|
|
||||||
* "or" = a match on ANY selected value, "and" = must match ALL. Provide BOTH to show it; it appears
|
|
||||||
* only once 2+ values are selected (below that, any and all are the same). */
|
|
||||||
matchMode?: "or" | "and";
|
|
||||||
onMatchModeChange?: (mode: "or" | "and") => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Combobox({
|
export default function Combobox({
|
||||||
@@ -84,8 +79,6 @@ export default function Combobox({
|
|||||||
maxVisible = 50,
|
maxVisible = 50,
|
||||||
renderOption,
|
renderOption,
|
||||||
selectedLabels,
|
selectedLabels,
|
||||||
matchMode,
|
|
||||||
onMatchModeChange,
|
|
||||||
}: ComboboxProps) {
|
}: ComboboxProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -299,32 +292,6 @@ export default function Combobox({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Any/all match segment (like SortControl's direction footer) — only meaningful, so only shown,
|
|
||||||
once 2+ values are picked. mousedown-preventDefault keeps the input focused so the list stays
|
|
||||||
open in the combobox's focus model. */}
|
|
||||||
{matchMode != null && onMatchModeChange && selected.length >= 2 && (
|
|
||||||
<div className="mt-1 flex items-center gap-2 border-t border-border px-1 pt-2">
|
|
||||||
<span className="text-2xs text-muted">{t("common.matchMode")}</span>
|
|
||||||
<div className="ml-auto flex gap-1">
|
|
||||||
{(["or", "and"] as const).map((m) => (
|
|
||||||
<button
|
|
||||||
key={m}
|
|
||||||
type="button"
|
|
||||||
aria-pressed={matchMode === m}
|
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
|
||||||
onClick={() => onMatchModeChange(m)}
|
|
||||||
className={`rounded-md border px-2 py-0.5 text-2xs transition ${
|
|
||||||
matchMode === m
|
|
||||||
? "border-accent bg-accent/15 text-accent"
|
|
||||||
: "border-border text-muted hover:border-accent/50 hover:text-fg"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{t(m === "or" ? "sidebar.any" : "sidebar.all")}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</PopoverSurface>
|
</PopoverSurface>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||||
|
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
||||||
|
|
||||||
|
vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (k: string) => k }) }));
|
||||||
|
|
||||||
|
import MatchToggle from "./MatchToggle";
|
||||||
|
|
||||||
|
afterEach(cleanup);
|
||||||
|
|
||||||
|
// The any/all switch sits on the panel above its picker (the owner, 2026-09-27): shown from two
|
||||||
|
// picks — with one, any and all are the same filter — as one named segment, the current mode pressed.
|
||||||
|
describe("MatchToggle", () => {
|
||||||
|
it("draws nothing below two picks", () => {
|
||||||
|
const { container } = render(<MatchToggle picked={1} value="any" onChange={() => {}} />);
|
||||||
|
expect(container.innerHTML).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("from two picks: a named any/all group with the current mode pressed, reporting the pick", () => {
|
||||||
|
const onChange = vi.fn();
|
||||||
|
render(<MatchToggle picked={2} value="any" onChange={onChange} />);
|
||||||
|
const group = screen.getByRole("group", { name: "common.matchMode" });
|
||||||
|
expect(group.querySelector('[aria-pressed="true"]')?.textContent).toBe("common.match.any");
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.match.all" }));
|
||||||
|
expect(onChange).toHaveBeenCalledWith("all");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import SegmentedToggle from "./SegmentedToggle";
|
||||||
|
|
||||||
|
export type MatchMode = "any" | "all";
|
||||||
|
|
||||||
|
// The any/all switch of a multi-value filter (tags, topics, languages, genres, collections, actors):
|
||||||
|
// a captioned segment on the panel, right ABOVE its picker — never in the picker's popover, where a
|
||||||
|
// closed picker hid that "all" was on (the owner, 2026-09-27; DESIGN "Action placement": the state
|
||||||
|
// that shapes the results stays in sight). With fewer than two picks any and all are the same filter,
|
||||||
|
// so it draws nothing then. The panel's captioned toggle is the one 24px segment (DESIGN log, S3g).
|
||||||
|
export default function MatchToggle({
|
||||||
|
picked,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
/** How many values the picker holds; the switch shows from two. */
|
||||||
|
picked: number;
|
||||||
|
value: MatchMode;
|
||||||
|
onChange: (mode: MatchMode) => void;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
if (picked < 2) return null;
|
||||||
|
return (
|
||||||
|
<div className={className ?? "mb-1.5"}>
|
||||||
|
<SegmentedToggle
|
||||||
|
size="sm"
|
||||||
|
label={t("common.matchMode")}
|
||||||
|
ariaLabel={t("common.matchMode")}
|
||||||
|
options={[
|
||||||
|
{ value: "any", label: t("common.match.any") },
|
||||||
|
{ value: "all", label: t("common.match.all") },
|
||||||
|
]}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -34,6 +34,10 @@
|
|||||||
},
|
},
|
||||||
"moreRefine": "{{count}} more — refine with search",
|
"moreRefine": "{{count}} more — refine with search",
|
||||||
"matchMode": "Match",
|
"matchMode": "Match",
|
||||||
|
"match": {
|
||||||
|
"any": "Any",
|
||||||
|
"all": "All"
|
||||||
|
},
|
||||||
"removeNamed": "Remove {{name}}",
|
"removeNamed": "Remove {{name}}",
|
||||||
"resetNamed": "Reset {{name}}",
|
"resetNamed": "Reset {{name}}",
|
||||||
"dnd": {
|
"dnd": {
|
||||||
|
|||||||
@@ -92,10 +92,6 @@
|
|||||||
"1y": "1 year"
|
"1y": "1 year"
|
||||||
},
|
},
|
||||||
"contentRating": "Age rating",
|
"contentRating": "Age rating",
|
||||||
"match": {
|
|
||||||
"any": "Any",
|
|
||||||
"all": "All"
|
|
||||||
},
|
|
||||||
"dir": {
|
"dir": {
|
||||||
"desc": "↓ Descending",
|
"desc": "↓ Descending",
|
||||||
"asc": "↑ Ascending"
|
"asc": "↑ Ascending"
|
||||||
@@ -103,7 +99,6 @@
|
|||||||
"collection": "Collection",
|
"collection": "Collection",
|
||||||
"collectionSearch": "Search collections…",
|
"collectionSearch": "Search collections…",
|
||||||
"collectionEmpty": "No collections",
|
"collectionEmpty": "No collections",
|
||||||
"matchLabel": "Match",
|
|
||||||
"studio": "Studio",
|
"studio": "Studio",
|
||||||
"playable": "Playback",
|
"playable": "Playback",
|
||||||
"playableOpt": {
|
"playableOpt": {
|
||||||
|
|||||||
@@ -14,8 +14,6 @@
|
|||||||
"pin": "Pin panel open",
|
"pin": "Pin panel open",
|
||||||
"unpin": "Unpin panel",
|
"unpin": "Unpin panel",
|
||||||
"openPanel": "Open {{title}}",
|
"openPanel": "Open {{title}}",
|
||||||
"any": "Any",
|
|
||||||
"all": "All",
|
|
||||||
"from": "From",
|
"from": "From",
|
||||||
"to": "To",
|
"to": "To",
|
||||||
"clearDates": "clear dates",
|
"clearDates": "clear dates",
|
||||||
|
|||||||
@@ -34,6 +34,10 @@
|
|||||||
},
|
},
|
||||||
"moreRefine": "még {{count}} — finomíts kereséssel",
|
"moreRefine": "még {{count}} — finomíts kereséssel",
|
||||||
"matchMode": "Egyezés",
|
"matchMode": "Egyezés",
|
||||||
|
"match": {
|
||||||
|
"any": "Bármelyik",
|
||||||
|
"all": "Mind"
|
||||||
|
},
|
||||||
"removeNamed": "{{name}}: eltávolítás",
|
"removeNamed": "{{name}}: eltávolítás",
|
||||||
"resetNamed": "{{name}}: alaphelyzet",
|
"resetNamed": "{{name}}: alaphelyzet",
|
||||||
"dnd": {
|
"dnd": {
|
||||||
|
|||||||
@@ -92,10 +92,6 @@
|
|||||||
"1y": "1 év"
|
"1y": "1 év"
|
||||||
},
|
},
|
||||||
"contentRating": "Korhatár",
|
"contentRating": "Korhatár",
|
||||||
"match": {
|
|
||||||
"any": "Bármelyik",
|
|
||||||
"all": "Mind"
|
|
||||||
},
|
|
||||||
"dir": {
|
"dir": {
|
||||||
"desc": "↓ Csökkenő",
|
"desc": "↓ Csökkenő",
|
||||||
"asc": "↑ Növekvő"
|
"asc": "↑ Növekvő"
|
||||||
@@ -103,7 +99,6 @@
|
|||||||
"collection": "Kollekció",
|
"collection": "Kollekció",
|
||||||
"collectionSearch": "Kollekciók keresése…",
|
"collectionSearch": "Kollekciók keresése…",
|
||||||
"collectionEmpty": "Nincs kollekció",
|
"collectionEmpty": "Nincs kollekció",
|
||||||
"matchLabel": "Egyezés",
|
|
||||||
"studio": "Stúdió",
|
"studio": "Stúdió",
|
||||||
"playable": "Lejátszás",
|
"playable": "Lejátszás",
|
||||||
"playableOpt": {
|
"playableOpt": {
|
||||||
|
|||||||
@@ -14,8 +14,6 @@
|
|||||||
"pin": "Panel rögzítése nyitva",
|
"pin": "Panel rögzítése nyitva",
|
||||||
"unpin": "Panel rögzítésének feloldása",
|
"unpin": "Panel rögzítésének feloldása",
|
||||||
"openPanel": "{{title}} megnyitása",
|
"openPanel": "{{title}} megnyitása",
|
||||||
"any": "Bármelyik",
|
|
||||||
"all": "Összes",
|
|
||||||
"from": "Ettől",
|
"from": "Ettől",
|
||||||
"to": "Eddig",
|
"to": "Eddig",
|
||||||
"clearDates": "dátumok törlése",
|
"clearDates": "dátumok törlése",
|
||||||
|
|||||||
Reference in New Issue
Block a user