mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
Merge pull request #1791 from caduHD4/feat/source-filter
feat: add filter by source in modal repacks
This commit is contained in:
@@ -292,7 +292,9 @@
|
|||||||
"historical_keyshop": "Historical keyshop",
|
"historical_keyshop": "Historical keyshop",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"caption": "Caption",
|
"caption": "Caption",
|
||||||
"audio": "Audio"
|
"audio": "Audio",
|
||||||
|
"filter_by_source": "Filter by source",
|
||||||
|
"no_repacks_found": "No sources found for this game"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
|
|||||||
@@ -235,6 +235,8 @@
|
|||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"caption": "Legenda",
|
"caption": "Legenda",
|
||||||
"audio": "Áudio",
|
"audio": "Áudio",
|
||||||
|
"filter_by_source": "Filtrar por fonte",
|
||||||
|
"no_repacks_found": "Nenhuma fonte encontrada para este jogo",
|
||||||
"edit_game_modal_button": "Alterar detalhes do jogo",
|
"edit_game_modal_button": "Alterar detalhes do jogo",
|
||||||
"game_added_to_pinned": "Jogo adicionado aos fixados",
|
"game_added_to_pinned": "Jogo adicionado aos fixados",
|
||||||
"game_removed_from_pinned": "Jogo removido dos fixados",
|
"game_removed_from_pinned": "Jogo removido dos fixados",
|
||||||
|
|||||||
@@ -2,7 +2,32 @@
|
|||||||
|
|
||||||
.repacks-modal {
|
.repacks-modal {
|
||||||
&__filter-container {
|
&__filter-container {
|
||||||
margin-bottom: calc(globals.$spacing-unit * 2);
|
margin-bottom: 1rem;
|
||||||
|
transition: min-height 0.3s ease;
|
||||||
|
|
||||||
|
&--drawer-open {
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__filter-top {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__filter-toggle {
|
||||||
|
align-self: flex-start;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: globals.$small-font-size;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__repacks {
|
&__repacks {
|
||||||
@@ -29,4 +54,101 @@
|
|||||||
&__repack-info {
|
&__repack-info {
|
||||||
font-size: globals.$small-font-size;
|
font-size: globals.$small-font-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__no-results {
|
||||||
|
width: 100%;
|
||||||
|
padding: calc(globals.$spacing-unit * 4) 0;
|
||||||
|
text-align: center;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
font-size: globals.$small-font-size;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-results-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit * 1.5);
|
||||||
|
max-width: 480px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-results-text {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
font-size: globals.$small-font-size;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-results-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__download-sources {
|
||||||
|
padding: 0;
|
||||||
|
background-color: var(--color-background-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
margin-top: calc(globals.$spacing-unit * 0.5);
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
max-height 0.3s ease,
|
||||||
|
padding 0.3s ease;
|
||||||
|
|
||||||
|
&--open {
|
||||||
|
padding: 0.75rem;
|
||||||
|
max-height: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__filter-label {
|
||||||
|
display: none;
|
||||||
|
font-size: globals.$small-font-size;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__source-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
align-items: start;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__source-item {
|
||||||
|
padding: 0.35rem 0.5rem;
|
||||||
|
background: var(--color-surface, rgba(0, 0, 0, 0.03));
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__source-item :global(.checkbox-field) {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__source-item :global(.checkbox-field__label) {
|
||||||
|
white-space: normal;
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: unset;
|
||||||
|
display: block;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
width: 100%;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { useContext, useEffect, useMemo, useState } from "react";
|
import { useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
PlusCircleIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronUpIcon,
|
||||||
|
} from "@primer/octicons-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@@ -7,7 +13,10 @@ import {
|
|||||||
DebridBadge,
|
DebridBadge,
|
||||||
Modal,
|
Modal,
|
||||||
TextField,
|
TextField,
|
||||||
|
CheckboxField,
|
||||||
} from "@renderer/components";
|
} from "@renderer/components";
|
||||||
|
import { downloadSourcesTable } from "@renderer/dexie";
|
||||||
|
import type { DownloadSource } from "@types";
|
||||||
import type { GameRepack } from "@types";
|
import type { GameRepack } from "@types";
|
||||||
|
|
||||||
import { DownloadSettingsModal } from "./download-settings-modal";
|
import { DownloadSettingsModal } from "./download-settings-modal";
|
||||||
@@ -36,6 +45,11 @@ export function RepacksModal({
|
|||||||
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
|
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
|
||||||
const [repack, setRepack] = useState<GameRepack | null>(null);
|
const [repack, setRepack] = useState<GameRepack | null>(null);
|
||||||
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
||||||
|
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
|
||||||
|
const [selectedFingerprints, setSelectedFingerprints] = useState<string[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const [filterTerm, setFilterTerm] = useState("");
|
||||||
|
|
||||||
const [hashesInDebrid, setHashesInDebrid] = useState<Record<string, boolean>>(
|
const [hashesInDebrid, setHashesInDebrid] = useState<Record<string, boolean>>(
|
||||||
{}
|
{}
|
||||||
@@ -46,6 +60,7 @@ export function RepacksModal({
|
|||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const { formatDate } = useDate();
|
const { formatDate } = useDate();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const getHashFromMagnet = (magnet: string) => {
|
const getHashFromMagnet = (magnet: string) => {
|
||||||
if (!magnet || typeof magnet !== "string") {
|
if (!magnet || typeof magnet !== "string") {
|
||||||
@@ -90,8 +105,37 @@ export function RepacksModal({
|
|||||||
}, [repacks, hashesInDebrid]);
|
}, [repacks, hashesInDebrid]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilteredRepacks(sortedRepacks);
|
downloadSourcesTable.toArray().then((sources) => {
|
||||||
}, [sortedRepacks, visible, game]);
|
const uniqueRepackers = new Set(sortedRepacks.map((r) => r.repacker));
|
||||||
|
const filteredSources = sources.filter(
|
||||||
|
(s) => s.name && uniqueRepackers.has(s.name) && !!s.fingerprint
|
||||||
|
);
|
||||||
|
setDownloadSources(filteredSources);
|
||||||
|
});
|
||||||
|
}, [sortedRepacks]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const term = filterTerm.trim().toLowerCase();
|
||||||
|
|
||||||
|
const byTerm = sortedRepacks.filter((repack) => {
|
||||||
|
if (!term) return true;
|
||||||
|
const lowerTitle = repack.title.toLowerCase();
|
||||||
|
const lowerRepacker = repack.repacker.toLowerCase();
|
||||||
|
return lowerTitle.includes(term) || lowerRepacker.includes(term);
|
||||||
|
});
|
||||||
|
|
||||||
|
const bySource = byTerm.filter((repack) => {
|
||||||
|
if (selectedFingerprints.length === 0) return true;
|
||||||
|
|
||||||
|
return downloadSources.some(
|
||||||
|
(src) =>
|
||||||
|
selectedFingerprints.includes(src.fingerprint) &&
|
||||||
|
src.name === repack.repacker
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
setFilteredRepacks(bySource);
|
||||||
|
}, [sortedRepacks, filterTerm, selectedFingerprints, downloadSources]);
|
||||||
|
|
||||||
const handleRepackClick = (repack: GameRepack) => {
|
const handleRepackClick = (repack: GameRepack) => {
|
||||||
setRepack(repack);
|
setRepack(repack);
|
||||||
@@ -99,17 +143,14 @@ export function RepacksModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||||
const term = event.target.value.toLocaleLowerCase();
|
setFilterTerm(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
setFilteredRepacks(
|
const toggleFingerprint = (fingerprint: string) => {
|
||||||
sortedRepacks.filter((repack) => {
|
setSelectedFingerprints((prev) =>
|
||||||
const lowerCaseTitle = repack.title.toLowerCase();
|
prev.includes(fingerprint)
|
||||||
const lowerCaseRepacker = repack.repacker.toLowerCase();
|
? prev.filter((f) => f !== fingerprint)
|
||||||
|
: [...prev, fingerprint]
|
||||||
return [lowerCaseTitle, lowerCaseRepacker].some((value) =>
|
|
||||||
value.includes(term)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,6 +159,8 @@ export function RepacksModal({
|
|||||||
return repack.uris.some((uri) => uri.includes(game.download!.uri));
|
return repack.uris.some((uri) => uri.includes(game.download!.uri));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DownloadSettingsModal
|
<DownloadSettingsModal
|
||||||
@@ -133,38 +176,101 @@ export function RepacksModal({
|
|||||||
description={t("repacks_modal_description")}
|
description={t("repacks_modal_description")}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div className="repacks-modal__filter-container">
|
<div
|
||||||
<TextField placeholder={t("filter")} onChange={handleFilter} />
|
className={`repacks-modal__filter-container ${isFilterDrawerOpen ? "repacks-modal__filter-container--drawer-open" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="repacks-modal__filter-top">
|
||||||
|
<TextField placeholder={t("filter")} onChange={handleFilter} />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={() => setIsFilterDrawerOpen(!isFilterDrawerOpen)}
|
||||||
|
className="repacks-modal__filter-toggle"
|
||||||
|
>
|
||||||
|
{t("filter_by_source")}
|
||||||
|
{isFilterDrawerOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`repacks-modal__download-sources ${isFilterDrawerOpen ? "repacks-modal__download-sources--open" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="repacks-modal__source-grid">
|
||||||
|
{downloadSources.map((source) => {
|
||||||
|
const label = source.name || source.url;
|
||||||
|
const truncatedLabel =
|
||||||
|
label.length > 16 ? label.substring(0, 16) + "..." : label;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={source.fingerprint}
|
||||||
|
className="repacks-modal__source-item"
|
||||||
|
>
|
||||||
|
<CheckboxField
|
||||||
|
label={truncatedLabel}
|
||||||
|
checked={selectedFingerprints.includes(
|
||||||
|
source.fingerprint
|
||||||
|
)}
|
||||||
|
onChange={() => toggleFingerprint(source.fingerprint)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="repacks-modal__repacks">
|
<div className="repacks-modal__repacks">
|
||||||
{filteredRepacks.map((repack) => {
|
{filteredRepacks.length === 0 ? (
|
||||||
const isLastDownloadedOption = checkIfLastDownloadedOption(repack);
|
<div className="repacks-modal__no-results">
|
||||||
|
<div className="repacks-modal__no-results-content">
|
||||||
|
<div className="repacks-modal__no-results-text">
|
||||||
|
{t("no_repacks_found")}
|
||||||
|
</div>
|
||||||
|
<div className="repacks-modal__no-results-button">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="primary"
|
||||||
|
onClick={() => {
|
||||||
|
onClose();
|
||||||
|
navigate("/settings?tab=2");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusCircleIcon />
|
||||||
|
{t("add_download_source", { ns: "settings" })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredRepacks.map((repack) => {
|
||||||
|
const isLastDownloadedOption =
|
||||||
|
checkIfLastDownloadedOption(repack);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={repack.id}
|
key={repack.id}
|
||||||
theme="dark"
|
theme="dark"
|
||||||
onClick={() => handleRepackClick(repack)}
|
onClick={() => handleRepackClick(repack)}
|
||||||
className="repacks-modal__repack-button"
|
className="repacks-modal__repack-button"
|
||||||
>
|
>
|
||||||
<p className="repacks-modal__repack-title">{repack.title}</p>
|
<p className="repacks-modal__repack-title">{repack.title}</p>
|
||||||
|
|
||||||
{isLastDownloadedOption && (
|
{isLastDownloadedOption && (
|
||||||
<Badge>{t("last_downloaded_option")}</Badge>
|
<Badge>{t("last_downloaded_option")}</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="repacks-modal__repack-info">
|
<p className="repacks-modal__repack-info">
|
||||||
{repack.fileSize} - {repack.repacker} -{" "}
|
{repack.fileSize} - {repack.repacker} -{" "}
|
||||||
{repack.uploadDate ? formatDate(repack.uploadDate) : ""}
|
{repack.uploadDate ? formatDate(repack.uploadDate) : ""}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{hashesInDebrid[getHashFromMagnet(repack.uris[0]) ?? ""] && (
|
{hashesInDebrid[getHashFromMagnet(repack.uris[0]) ?? ""] && (
|
||||||
<DebridBadge />
|
<DebridBadge />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user