mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feature: add collections section
This commit is contained in:
parent
6fce60f9f7
commit
b7cabfdbde
26 changed files with 485 additions and 7 deletions
|
@ -194,6 +194,19 @@
|
|||
"found_download_option_other": "Found {{countFormatted}} download options",
|
||||
"import": "Import"
|
||||
},
|
||||
"collections": {
|
||||
"collections": "Collections",
|
||||
"add_the_game_to_the_collection": "Add the game to the collection",
|
||||
"select_a_collection": "Select a collection",
|
||||
"enter_the_name_of_the_collection": "Enter the name of the collection",
|
||||
"add": "Add",
|
||||
"remove": "Remove",
|
||||
"you_cant_give_collections_existing_or_empty_names": "You can`t give collections existing or empty names",
|
||||
"the_collection_has_been_added_successfully": "The collection has been added successfully",
|
||||
"the_collection_has_been_removed_successfully": "The collection has been removed successfully",
|
||||
"the_game_has_been_added_to_the_collection": "The game has been added to the collection",
|
||||
"the_game_has_been_removed_from_the_collection": "The game has been removed from the collection"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
"game_ready_to_install": "{{title}} is ready to install",
|
||||
|
|
|
@ -192,6 +192,19 @@
|
|||
"found_download_option_other": "Найдено {{countFormatted}} вариантов загрузки",
|
||||
"import": "Импортировать"
|
||||
},
|
||||
"collections": {
|
||||
"collections": "Коллекции",
|
||||
"add_the_game_to_the_collection": "Добавьте игру в коллекцию",
|
||||
"select_a_collection": "Выберите коллекцию",
|
||||
"enter_the_name_of_the_collection": "Введите название коллекции",
|
||||
"add": "Добавить",
|
||||
"remove": "Удалить",
|
||||
"you_cant_give_collections_existing_or_empty_names": "Нельзя давать коллекциям существующие или пустые названия",
|
||||
"the_collection_has_been_added_successfully": "Коллекция успешно добавлена",
|
||||
"the_collection_has_been_removed_successfully": "Коллекция успешно удалена",
|
||||
"the_game_has_been_added_to_the_collection": "Игра добавлена в коллекцию",
|
||||
"the_game_has_been_removed_from_the_collection": "Игра удалена из коллекции"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Загрузка завершена",
|
||||
"game_ready_to_install": "{{title}} готова к установке",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { DataSource } from "typeorm";
|
||||
import {
|
||||
Collection,
|
||||
DownloadQueue,
|
||||
DownloadSource,
|
||||
Game,
|
||||
|
@ -19,6 +20,7 @@ export const createDataSource = (
|
|||
new DataSource({
|
||||
type: "better-sqlite3",
|
||||
entities: [
|
||||
Collection,
|
||||
Game,
|
||||
Repack,
|
||||
UserPreferences,
|
||||
|
|
21
src/main/entity/collection.entity.ts
Normal file
21
src/main/entity/collection.entity.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
} from "typeorm";
|
||||
import { Game } from "./game.entity";
|
||||
|
||||
@Entity("collection")
|
||||
export class Collection {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column("text", { unique: true })
|
||||
title: string;
|
||||
|
||||
@ManyToMany("Game", "collections")
|
||||
@JoinTable()
|
||||
games: Game[];
|
||||
}
|
|
@ -6,12 +6,14 @@ import {
|
|||
UpdateDateColumn,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
ManyToMany,
|
||||
} from "typeorm";
|
||||
import { Repack } from "./repack.entity";
|
||||
|
||||
import type { GameShop, GameStatus } from "@types";
|
||||
import { Downloader } from "@shared";
|
||||
import type { DownloadQueue } from "./download-queue.entity";
|
||||
import { Collection } from "./collection.entity";
|
||||
|
||||
@Entity("game")
|
||||
export class Game {
|
||||
|
@ -79,6 +81,9 @@ export class Game {
|
|||
@OneToOne("DownloadQueue", "game")
|
||||
downloadQueue: DownloadQueue;
|
||||
|
||||
@ManyToMany("Collection", "games")
|
||||
collections: Collection[];
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
isDeleted: boolean;
|
||||
|
||||
|
|
|
@ -4,4 +4,5 @@ export * from "./user-preferences.entity";
|
|||
export * from "./game-shop-cache.entity";
|
||||
export * from "./download-source.entity";
|
||||
export * from "./download-queue.entity";
|
||||
export * from "./collection.entity";
|
||||
export * from "./user-auth";
|
||||
|
|
18
src/main/events/collections/add-collection-game.ts
Normal file
18
src/main/events/collections/add-collection-game.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { collectionRepository } from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { Collection, Game } from "@main/entity";
|
||||
|
||||
const addCollectionGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
collectionId: number,
|
||||
game: Game
|
||||
) => {
|
||||
return await collectionRepository
|
||||
.createQueryBuilder()
|
||||
.relation(Collection, "games")
|
||||
.of(collectionId)
|
||||
.add(game);
|
||||
};
|
||||
|
||||
registerEvent("addCollectionGame", addCollectionGame);
|
14
src/main/events/collections/add-collection.ts
Normal file
14
src/main/events/collections/add-collection.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { collectionRepository } from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const addCollection = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
title: string
|
||||
) => {
|
||||
return await collectionRepository.insert({
|
||||
title: title,
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("addCollection", addCollection);
|
17
src/main/events/collections/get-collections.ts
Normal file
17
src/main/events/collections/get-collections.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { collectionRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getCollections = async () =>
|
||||
collectionRepository.find({
|
||||
relations: {
|
||||
games: {
|
||||
downloadQueue: true,
|
||||
repack: true,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
title: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
registerEvent("getCollections", getCollections);
|
18
src/main/events/collections/remove-collection-game.ts
Normal file
18
src/main/events/collections/remove-collection-game.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { collectionRepository } from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { Collection, Game } from "@main/entity";
|
||||
|
||||
const removeCollectionGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
collectionId: number,
|
||||
game: Game
|
||||
) => {
|
||||
return await collectionRepository
|
||||
.createQueryBuilder()
|
||||
.relation(Collection, "games")
|
||||
.of(collectionId)
|
||||
.remove(game);
|
||||
};
|
||||
|
||||
registerEvent("removeCollectionGame", removeCollectionGame);
|
13
src/main/events/collections/remove-collection.ts
Normal file
13
src/main/events/collections/remove-collection.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { collectionRepository } from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { Collection } from "@main/entity";
|
||||
|
||||
const removeCollection = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
collection: Collection
|
||||
) => {
|
||||
return await collectionRepository.remove(collection);
|
||||
};
|
||||
|
||||
registerEvent("removeCollection", removeCollection);
|
|
@ -8,6 +8,11 @@ import "./catalogue/get-how-long-to-beat";
|
|||
import "./catalogue/get-random-game";
|
||||
import "./catalogue/search-games";
|
||||
import "./catalogue/search-game-repacks";
|
||||
import "./collections/add-collection";
|
||||
import "./collections/add-collection-game";
|
||||
import "./collections/get-collections";
|
||||
import "./collections/remove-collection";
|
||||
import "./collections/remove-collection-game";
|
||||
import "./hardware/get-disk-free-space";
|
||||
import "./library/add-game-to-library";
|
||||
import "./library/create-game-shortcut";
|
||||
|
|
|
@ -8,6 +8,7 @@ const getLibrary = async () =>
|
|||
},
|
||||
relations: {
|
||||
downloadQueue: true,
|
||||
collections: true,
|
||||
},
|
||||
order: {
|
||||
createdAt: "desc",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { dataSource } from "./data-source";
|
||||
import {
|
||||
Collection,
|
||||
DownloadQueue,
|
||||
DownloadSource,
|
||||
Game,
|
||||
|
@ -24,3 +25,5 @@ export const downloadSourceRepository =
|
|||
export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
||||
|
||||
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
||||
|
||||
export const collectionRepository = dataSource.getRepository(Collection);
|
||||
|
|
|
@ -9,6 +9,8 @@ import type {
|
|||
AppUpdaterEvent,
|
||||
StartGameDownloadPayload,
|
||||
GameRunning,
|
||||
Collection,
|
||||
Game,
|
||||
} from "@types";
|
||||
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
|
@ -102,6 +104,16 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
ipcRenderer.removeListener("on-library-batch-complete", listener);
|
||||
},
|
||||
|
||||
/* Collections */
|
||||
addCollection: (title: string) => ipcRenderer.invoke("addCollection", title),
|
||||
addCollectionGame: (id: number, game: Game) =>
|
||||
ipcRenderer.invoke("addCollectionGame", id, game),
|
||||
getCollections: () => ipcRenderer.invoke("getCollections"),
|
||||
removeCollection: (collection: Collection) =>
|
||||
ipcRenderer.invoke("removeCollection", collection),
|
||||
removeCollectionGame: (id: number, game: Game) =>
|
||||
ipcRenderer.invoke("removeCollectionGame", id, game),
|
||||
|
||||
/* Hardware */
|
||||
getDiskFreeSpace: (path: string) =>
|
||||
ipcRenderer.invoke("getDiskFreeSpace", path),
|
||||
|
|
|
@ -15,6 +15,7 @@ import { buildGameDetailsPath } from "@renderer/helpers";
|
|||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import { SidebarProfile } from "./sidebar-profile";
|
||||
import { sortBy } from "lodash-es";
|
||||
import { useCollections } from "@renderer/hooks/use-collections";
|
||||
|
||||
const SIDEBAR_MIN_WIDTH = 200;
|
||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||
|
@ -25,6 +26,7 @@ const initialSidebarWidth = window.localStorage.getItem("sidebarWidth");
|
|||
export function Sidebar() {
|
||||
const { t } = useTranslation("sidebar");
|
||||
const { library, updateLibrary } = useLibrary();
|
||||
const { collections, updateCollections } = useCollections();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [filteredLibrary, setFilteredLibrary] = useState<LibraryGame[]>([]);
|
||||
|
@ -33,6 +35,7 @@ export function Sidebar() {
|
|||
const [sidebarWidth, setSidebarWidth] = useState(
|
||||
initialSidebarWidth ? Number(initialSidebarWidth) : SIDEBAR_INITIAL_WIDTH
|
||||
);
|
||||
const [showCollections, setShowCollections] = useState(true);
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
|
@ -48,6 +51,10 @@ export function Sidebar() {
|
|||
updateLibrary();
|
||||
}, [lastPacket?.game.id, updateLibrary]);
|
||||
|
||||
useEffect(() => {
|
||||
updateCollections();
|
||||
});
|
||||
|
||||
const isDownloading = sortedLibrary.some(
|
||||
(game) => game.status === "active" && game.progress !== 1
|
||||
);
|
||||
|
@ -67,17 +74,19 @@ export function Sidebar() {
|
|||
};
|
||||
|
||||
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
const val = event.target.value.toLocaleLowerCase();
|
||||
|
||||
setFilteredLibrary(
|
||||
sortedLibrary.filter((game) =>
|
||||
game.title
|
||||
.toLowerCase()
|
||||
.includes(event.target.value.toLocaleLowerCase())
|
||||
)
|
||||
sortedLibrary.filter((game) => game.title.toLowerCase().includes(val))
|
||||
);
|
||||
|
||||
setShowCollections(val == "");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredLibrary(sortedLibrary);
|
||||
setFilteredLibrary(
|
||||
sortedLibrary.filter((game) => !game.collections.length)
|
||||
);
|
||||
}, [sortedLibrary]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -199,6 +208,52 @@ export function Sidebar() {
|
|||
theme="dark"
|
||||
/>
|
||||
|
||||
{collections.map((collection) =>
|
||||
collection.games?.length && showCollections ? (
|
||||
<section className={styles.section} key={collection.id}>
|
||||
<small className={styles.sectionTitle}>
|
||||
{collection.title}
|
||||
</small>
|
||||
|
||||
<ul className={styles.menu}>
|
||||
{collection.games.map((game) => (
|
||||
<li
|
||||
key={game.id}
|
||||
className={styles.menuItem({
|
||||
active:
|
||||
location.pathname ===
|
||||
`/game/${game.shop}/${game.objectID}`,
|
||||
muted: game.status === "removed",
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuItemButton}
|
||||
onClick={(event) =>
|
||||
handleSidebarGameClick(event, game)
|
||||
}
|
||||
>
|
||||
{game.iconUrl ? (
|
||||
<img
|
||||
className={styles.gameIcon}
|
||||
src={game.iconUrl}
|
||||
alt={game.title}
|
||||
/>
|
||||
) : (
|
||||
<SteamLogo className={styles.gameIcon} />
|
||||
)}
|
||||
|
||||
<span className={styles.menuItemButtonLabel}>
|
||||
{getGameTitle(game)}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null
|
||||
)}
|
||||
|
||||
<ul className={styles.menu}>
|
||||
{filteredLibrary.map((game) => (
|
||||
<li
|
||||
|
|
8
src/renderer/src/declaration.d.ts
vendored
8
src/renderer/src/declaration.d.ts
vendored
|
@ -14,6 +14,7 @@ import type {
|
|||
RealDebridUser,
|
||||
DownloadSource,
|
||||
UserProfile,
|
||||
Collection,
|
||||
} from "@types";
|
||||
import type { DiskSpace } from "check-disk-space";
|
||||
|
||||
|
@ -78,6 +79,13 @@ declare global {
|
|||
) => () => Electron.IpcRenderer;
|
||||
onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
|
||||
/* Collections */
|
||||
addCollection: (title: string) => Promise<void>;
|
||||
addCollectionGame: (id: number, game: Game) => Promise<void>;
|
||||
getCollections: () => Promise<Collection[]>;
|
||||
removeCollection: (collection: Collection) => Promise<void>;
|
||||
removeCollectionGame: (id: number, game: Game) => Promise<void>;
|
||||
|
||||
/* User preferences */
|
||||
getUserPreferences: () => Promise<UserPreferences | null>;
|
||||
updateUserPreferences: (
|
||||
|
|
26
src/renderer/src/features/collections-slice.ts
Normal file
26
src/renderer/src/features/collections-slice.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Collection } from "../../../types/index";
|
||||
|
||||
export interface CollectionsState {
|
||||
value: Collection[];
|
||||
}
|
||||
|
||||
const initialState: CollectionsState = {
|
||||
value: [],
|
||||
};
|
||||
|
||||
export const collectionsSlice = createSlice({
|
||||
name: "collections",
|
||||
initialState,
|
||||
reducers: {
|
||||
setCollections: (
|
||||
state,
|
||||
action: PayloadAction<CollectionsState["value"]>
|
||||
) => {
|
||||
state.value = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setCollections } = collectionsSlice.actions;
|
|
@ -6,3 +6,4 @@ export * from "./window-slice";
|
|||
export * from "./toast-slice";
|
||||
export * from "./user-details-slice";
|
||||
export * from "./running-game-slice";
|
||||
export * from "./collections-slice";
|
||||
|
|
|
@ -4,3 +4,4 @@ export * from "./use-date";
|
|||
export * from "./use-toast";
|
||||
export * from "./redux";
|
||||
export * from "./use-user-details";
|
||||
export * from "./use-collections";
|
||||
|
|
64
src/renderer/src/hooks/use-collections.ts
Normal file
64
src/renderer/src/hooks/use-collections.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { useCallback } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "./redux";
|
||||
import { setCollections } from "@renderer/features";
|
||||
import { Collection, Game } from "@types";
|
||||
import { useToast } from "./use-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function useCollections() {
|
||||
const { t } = useTranslation("collections");
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const collections = useAppSelector((state) => state.collections.value);
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
|
||||
const updateCollections = useCallback(async () => {
|
||||
return window.electron
|
||||
.getCollections()
|
||||
.then((updatedCollection) => dispatch(setCollections(updatedCollection)));
|
||||
}, [dispatch]);
|
||||
|
||||
const addCollection = async (title: string) => {
|
||||
if (
|
||||
!collections.some((collection) => collection.title === title) &&
|
||||
title !== ""
|
||||
) {
|
||||
await window.electron.addCollection(title);
|
||||
|
||||
updateCollections();
|
||||
showSuccessToast(t("the_collection_has_been_added_successfully"));
|
||||
} else {
|
||||
showErrorToast(t("you_cant_give_collections_existing_or_empty_names"));
|
||||
}
|
||||
};
|
||||
|
||||
const removeCollection = async (collection: Collection) => {
|
||||
await window.electron.removeCollection(collection);
|
||||
|
||||
updateCollections();
|
||||
showSuccessToast(t("the_collection_has_been_removed_successfully"));
|
||||
};
|
||||
|
||||
const addCollectionGame = async (collectionId: number, game: Game) => {
|
||||
await window.electron.addCollectionGame(collectionId, game);
|
||||
|
||||
updateCollections();
|
||||
showSuccessToast(t("the_game_has_been_added_to_the_collection"));
|
||||
};
|
||||
|
||||
const removeCollectionGame = async (collectionId: number, game: Game) => {
|
||||
await window.electron.removeCollectionGame(collectionId, game);
|
||||
|
||||
updateCollections();
|
||||
showSuccessToast(t("the_game_has_been_removed_from_the_collection"));
|
||||
};
|
||||
|
||||
return {
|
||||
collections,
|
||||
updateCollections,
|
||||
addCollection,
|
||||
removeCollection,
|
||||
addCollectionGame,
|
||||
removeCollectionGame,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT } from "../../../theme.css";
|
||||
|
||||
export const collectionsContainer = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
flexDirection: "column",
|
||||
width: "50%",
|
||||
margin: "auto",
|
||||
});
|
||||
|
||||
export const buttonsContainer = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
flexDirection: "row",
|
||||
});
|
||||
|
||||
export const buttonSelect = style({
|
||||
flex: 3,
|
||||
});
|
||||
|
||||
export const buttonRemove = style({
|
||||
flex: 1,
|
||||
});
|
108
src/renderer/src/pages/game-details/modals/collections-modal.tsx
Normal file
108
src/renderer/src/pages/game-details/modals/collections-modal.tsx
Normal file
|
@ -0,0 +1,108 @@
|
|||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Modal, TextField } from "@renderer/components";
|
||||
import type { Collection, Game } from "@types";
|
||||
import * as styles from "./collections-modal.css";
|
||||
import { useCollections } from "@renderer/hooks/use-collections";
|
||||
import { useLibrary } from "@renderer/hooks";
|
||||
|
||||
export interface CollectionsModalProps {
|
||||
visible: boolean;
|
||||
game: Game;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function CollectionsModal({
|
||||
visible,
|
||||
game,
|
||||
onClose,
|
||||
}: CollectionsModalProps) {
|
||||
const { t } = useTranslation("collections");
|
||||
const {
|
||||
collections,
|
||||
addCollection,
|
||||
removeCollection,
|
||||
addCollectionGame,
|
||||
removeCollectionGame,
|
||||
} = useCollections();
|
||||
const { updateLibrary } = useLibrary();
|
||||
|
||||
const [collectionTitle, setcollectionTitle] = useState<string>("");
|
||||
|
||||
const handleAddCollection = () => {
|
||||
addCollection(collectionTitle);
|
||||
setcollectionTitle("");
|
||||
};
|
||||
|
||||
const handleRemoveCollection = (collection: Collection) => {
|
||||
removeCollection(collection);
|
||||
updateLibrary();
|
||||
};
|
||||
|
||||
const handleSetCollection = (id: number, addOrRemove: boolean) => {
|
||||
addOrRemove ? addCollectionGame(id, game) : removeCollectionGame(id, game);
|
||||
updateLibrary();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("collections")}
|
||||
onClose={onClose}
|
||||
large={true}
|
||||
>
|
||||
<div className={styles.collectionsContainer}>
|
||||
<TextField
|
||||
value={collectionTitle}
|
||||
theme="dark"
|
||||
placeholder={t("enter_the_name_of_the_collection")}
|
||||
onChange={(e) => setcollectionTitle(e.target.value)}
|
||||
rightContent={
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={handleAddCollection}
|
||||
>
|
||||
{t("add")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{collections.map((collection) => (
|
||||
<div className={styles.buttonsContainer} key={collection.id}>
|
||||
<Button
|
||||
className={styles.buttonSelect}
|
||||
type="button"
|
||||
theme={
|
||||
collection.games?.some(
|
||||
(collectionGame) => collectionGame.id == game.id
|
||||
)
|
||||
? "primary"
|
||||
: "outline"
|
||||
}
|
||||
onClick={() =>
|
||||
handleSetCollection(
|
||||
collection.id,
|
||||
!collection.games?.some(
|
||||
(collectionGame) => collectionGame.id == game.id
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
{collection.title}
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.buttonRemove}
|
||||
type="button"
|
||||
theme="danger"
|
||||
onClick={() => handleRemoveCollection(collection)}
|
||||
>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -7,6 +7,7 @@ import { gameDetailsContext } from "@renderer/context";
|
|||
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
||||
import { useDownload, useToast } from "@renderer/hooks";
|
||||
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
||||
import { CollectionsModal } from "./collections-modal";
|
||||
|
||||
export interface GameOptionsModalProps {
|
||||
visible: boolean;
|
||||
|
@ -19,7 +20,7 @@ export function GameOptionsModal({
|
|||
game,
|
||||
onClose,
|
||||
}: GameOptionsModalProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
const { t } = useTranslation(["game_details", "collections"]);
|
||||
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
|
||||
|
@ -28,6 +29,7 @@ export function GameOptionsModal({
|
|||
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
|
||||
const [showCollectionsModal, setShowCollectionsModal] = useState(false);
|
||||
|
||||
const {
|
||||
removeGameInstaller,
|
||||
|
@ -107,6 +109,29 @@ export function GameOptionsModal({
|
|||
large={true}
|
||||
>
|
||||
<div className={styles.optionsContainer}>
|
||||
<div className={styles.gameOptionHeader}>
|
||||
<h2>{t("collections:collections")}</h2>
|
||||
<h4 className={styles.gameOptionHeaderDescription}>
|
||||
{t("collections:add_the_game_to_the_collection")}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={() => setShowCollectionsModal(true)}
|
||||
>
|
||||
{t("collections:select_a_collection")}
|
||||
</Button>
|
||||
|
||||
<CollectionsModal
|
||||
visible={showCollectionsModal}
|
||||
game={game}
|
||||
onClose={() => {
|
||||
setShowCollectionsModal(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={styles.gameOptionHeader}>
|
||||
<h2>{t("executable_section_title")}</h2>
|
||||
<h4 className={styles.gameOptionHeaderDescription}>
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
downloadSlice,
|
||||
windowSlice,
|
||||
librarySlice,
|
||||
collectionsSlice,
|
||||
searchSlice,
|
||||
userPreferencesSlice,
|
||||
toastSlice,
|
||||
|
@ -15,6 +16,7 @@ export const store = configureStore({
|
|||
search: searchSlice.reducer,
|
||||
window: windowSlice.reducer,
|
||||
library: librarySlice.reducer,
|
||||
collections: collectionsSlice.reducer,
|
||||
userPreferences: userPreferencesSlice.reducer,
|
||||
download: downloadSlice.reducer,
|
||||
toast: toastSlice.reducer,
|
||||
|
|
|
@ -128,12 +128,19 @@ export interface Game {
|
|||
objectID: string;
|
||||
shop: GameShop;
|
||||
downloadQueue: DownloadQueue | null;
|
||||
collections: Collection[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export type LibraryGame = Omit<Game, "repacks">;
|
||||
|
||||
export interface Collection {
|
||||
id: number;
|
||||
title: string;
|
||||
games: Game[];
|
||||
}
|
||||
|
||||
export interface GameRunning {
|
||||
id: number;
|
||||
title: string;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue