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",
|
"found_download_option_other": "Found {{countFormatted}} download options",
|
||||||
"import": "Import"
|
"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": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
"game_ready_to_install": "{{title}} is ready to install",
|
"game_ready_to_install": "{{title}} is ready to install",
|
||||||
|
|
|
@ -192,6 +192,19 @@
|
||||||
"found_download_option_other": "Найдено {{countFormatted}} вариантов загрузки",
|
"found_download_option_other": "Найдено {{countFormatted}} вариантов загрузки",
|
||||||
"import": "Импортировать"
|
"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": {
|
"notifications": {
|
||||||
"download_complete": "Загрузка завершена",
|
"download_complete": "Загрузка завершена",
|
||||||
"game_ready_to_install": "{{title}} готова к установке",
|
"game_ready_to_install": "{{title}} готова к установке",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { DataSource } from "typeorm";
|
import { DataSource } from "typeorm";
|
||||||
import {
|
import {
|
||||||
|
Collection,
|
||||||
DownloadQueue,
|
DownloadQueue,
|
||||||
DownloadSource,
|
DownloadSource,
|
||||||
Game,
|
Game,
|
||||||
|
@ -19,6 +20,7 @@ export const createDataSource = (
|
||||||
new DataSource({
|
new DataSource({
|
||||||
type: "better-sqlite3",
|
type: "better-sqlite3",
|
||||||
entities: [
|
entities: [
|
||||||
|
Collection,
|
||||||
Game,
|
Game,
|
||||||
Repack,
|
Repack,
|
||||||
UserPreferences,
|
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,
|
UpdateDateColumn,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
|
ManyToMany,
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
import { Repack } from "./repack.entity";
|
import { Repack } from "./repack.entity";
|
||||||
|
|
||||||
import type { GameShop, GameStatus } from "@types";
|
import type { GameShop, GameStatus } from "@types";
|
||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
import type { DownloadQueue } from "./download-queue.entity";
|
import type { DownloadQueue } from "./download-queue.entity";
|
||||||
|
import { Collection } from "./collection.entity";
|
||||||
|
|
||||||
@Entity("game")
|
@Entity("game")
|
||||||
export class Game {
|
export class Game {
|
||||||
|
@ -79,6 +81,9 @@ export class Game {
|
||||||
@OneToOne("DownloadQueue", "game")
|
@OneToOne("DownloadQueue", "game")
|
||||||
downloadQueue: DownloadQueue;
|
downloadQueue: DownloadQueue;
|
||||||
|
|
||||||
|
@ManyToMany("Collection", "games")
|
||||||
|
collections: Collection[];
|
||||||
|
|
||||||
@Column("boolean", { default: false })
|
@Column("boolean", { default: false })
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
|
|
||||||
|
|
|
@ -4,4 +4,5 @@ export * from "./user-preferences.entity";
|
||||||
export * from "./game-shop-cache.entity";
|
export * from "./game-shop-cache.entity";
|
||||||
export * from "./download-source.entity";
|
export * from "./download-source.entity";
|
||||||
export * from "./download-queue.entity";
|
export * from "./download-queue.entity";
|
||||||
|
export * from "./collection.entity";
|
||||||
export * from "./user-auth";
|
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/get-random-game";
|
||||||
import "./catalogue/search-games";
|
import "./catalogue/search-games";
|
||||||
import "./catalogue/search-game-repacks";
|
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 "./hardware/get-disk-free-space";
|
||||||
import "./library/add-game-to-library";
|
import "./library/add-game-to-library";
|
||||||
import "./library/create-game-shortcut";
|
import "./library/create-game-shortcut";
|
||||||
|
|
|
@ -8,6 +8,7 @@ const getLibrary = async () =>
|
||||||
},
|
},
|
||||||
relations: {
|
relations: {
|
||||||
downloadQueue: true,
|
downloadQueue: true,
|
||||||
|
collections: true,
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
createdAt: "desc",
|
createdAt: "desc",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { dataSource } from "./data-source";
|
import { dataSource } from "./data-source";
|
||||||
import {
|
import {
|
||||||
|
Collection,
|
||||||
DownloadQueue,
|
DownloadQueue,
|
||||||
DownloadSource,
|
DownloadSource,
|
||||||
Game,
|
Game,
|
||||||
|
@ -24,3 +25,5 @@ export const downloadSourceRepository =
|
||||||
export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
||||||
|
|
||||||
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
||||||
|
|
||||||
|
export const collectionRepository = dataSource.getRepository(Collection);
|
||||||
|
|
|
@ -9,6 +9,8 @@ import type {
|
||||||
AppUpdaterEvent,
|
AppUpdaterEvent,
|
||||||
StartGameDownloadPayload,
|
StartGameDownloadPayload,
|
||||||
GameRunning,
|
GameRunning,
|
||||||
|
Collection,
|
||||||
|
Game,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electron", {
|
contextBridge.exposeInMainWorld("electron", {
|
||||||
|
@ -102,6 +104,16 @@ contextBridge.exposeInMainWorld("electron", {
|
||||||
ipcRenderer.removeListener("on-library-batch-complete", listener);
|
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 */
|
/* Hardware */
|
||||||
getDiskFreeSpace: (path: string) =>
|
getDiskFreeSpace: (path: string) =>
|
||||||
ipcRenderer.invoke("getDiskFreeSpace", path),
|
ipcRenderer.invoke("getDiskFreeSpace", path),
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { SidebarProfile } from "./sidebar-profile";
|
import { SidebarProfile } from "./sidebar-profile";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
|
import { useCollections } from "@renderer/hooks/use-collections";
|
||||||
|
|
||||||
const SIDEBAR_MIN_WIDTH = 200;
|
const SIDEBAR_MIN_WIDTH = 200;
|
||||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||||
|
@ -25,6 +26,7 @@ const initialSidebarWidth = window.localStorage.getItem("sidebarWidth");
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const { t } = useTranslation("sidebar");
|
const { t } = useTranslation("sidebar");
|
||||||
const { library, updateLibrary } = useLibrary();
|
const { library, updateLibrary } = useLibrary();
|
||||||
|
const { collections, updateCollections } = useCollections();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [filteredLibrary, setFilteredLibrary] = useState<LibraryGame[]>([]);
|
const [filteredLibrary, setFilteredLibrary] = useState<LibraryGame[]>([]);
|
||||||
|
@ -33,6 +35,7 @@ export function Sidebar() {
|
||||||
const [sidebarWidth, setSidebarWidth] = useState(
|
const [sidebarWidth, setSidebarWidth] = useState(
|
||||||
initialSidebarWidth ? Number(initialSidebarWidth) : SIDEBAR_INITIAL_WIDTH
|
initialSidebarWidth ? Number(initialSidebarWidth) : SIDEBAR_INITIAL_WIDTH
|
||||||
);
|
);
|
||||||
|
const [showCollections, setShowCollections] = useState(true);
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
@ -48,6 +51,10 @@ export function Sidebar() {
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
}, [lastPacket?.game.id, updateLibrary]);
|
}, [lastPacket?.game.id, updateLibrary]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateCollections();
|
||||||
|
});
|
||||||
|
|
||||||
const isDownloading = sortedLibrary.some(
|
const isDownloading = sortedLibrary.some(
|
||||||
(game) => game.status === "active" && game.progress !== 1
|
(game) => game.status === "active" && game.progress !== 1
|
||||||
);
|
);
|
||||||
|
@ -67,17 +74,19 @@ export function Sidebar() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||||
|
const val = event.target.value.toLocaleLowerCase();
|
||||||
|
|
||||||
setFilteredLibrary(
|
setFilteredLibrary(
|
||||||
sortedLibrary.filter((game) =>
|
sortedLibrary.filter((game) => game.title.toLowerCase().includes(val))
|
||||||
game.title
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(event.target.value.toLocaleLowerCase())
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setShowCollections(val == "");
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilteredLibrary(sortedLibrary);
|
setFilteredLibrary(
|
||||||
|
sortedLibrary.filter((game) => !game.collections.length)
|
||||||
|
);
|
||||||
}, [sortedLibrary]);
|
}, [sortedLibrary]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -199,6 +208,52 @@ export function Sidebar() {
|
||||||
theme="dark"
|
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}>
|
<ul className={styles.menu}>
|
||||||
{filteredLibrary.map((game) => (
|
{filteredLibrary.map((game) => (
|
||||||
<li
|
<li
|
||||||
|
|
8
src/renderer/src/declaration.d.ts
vendored
8
src/renderer/src/declaration.d.ts
vendored
|
@ -14,6 +14,7 @@ import type {
|
||||||
RealDebridUser,
|
RealDebridUser,
|
||||||
DownloadSource,
|
DownloadSource,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
|
Collection,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { DiskSpace } from "check-disk-space";
|
import type { DiskSpace } from "check-disk-space";
|
||||||
|
|
||||||
|
@ -78,6 +79,13 @@ declare global {
|
||||||
) => () => Electron.IpcRenderer;
|
) => () => Electron.IpcRenderer;
|
||||||
onLibraryBatchComplete: (cb: () => void) => () => 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 */
|
/* User preferences */
|
||||||
getUserPreferences: () => Promise<UserPreferences | null>;
|
getUserPreferences: () => Promise<UserPreferences | null>;
|
||||||
updateUserPreferences: (
|
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 "./toast-slice";
|
||||||
export * from "./user-details-slice";
|
export * from "./user-details-slice";
|
||||||
export * from "./running-game-slice";
|
export * from "./running-game-slice";
|
||||||
|
export * from "./collections-slice";
|
||||||
|
|
|
@ -4,3 +4,4 @@ export * from "./use-date";
|
||||||
export * from "./use-toast";
|
export * from "./use-toast";
|
||||||
export * from "./redux";
|
export * from "./redux";
|
||||||
export * from "./use-user-details";
|
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 { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
||||||
import { useDownload, useToast } from "@renderer/hooks";
|
import { useDownload, useToast } from "@renderer/hooks";
|
||||||
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
||||||
|
import { CollectionsModal } from "./collections-modal";
|
||||||
|
|
||||||
export interface GameOptionsModalProps {
|
export interface GameOptionsModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -19,7 +20,7 @@ export function GameOptionsModal({
|
||||||
game,
|
game,
|
||||||
onClose,
|
onClose,
|
||||||
}: GameOptionsModalProps) {
|
}: GameOptionsModalProps) {
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation(["game_details", "collections"]);
|
||||||
|
|
||||||
const { showSuccessToast, showErrorToast } = useToast();
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ export function GameOptionsModal({
|
||||||
|
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
|
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
|
||||||
|
const [showCollectionsModal, setShowCollectionsModal] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
removeGameInstaller,
|
removeGameInstaller,
|
||||||
|
@ -107,6 +109,29 @@ export function GameOptionsModal({
|
||||||
large={true}
|
large={true}
|
||||||
>
|
>
|
||||||
<div className={styles.optionsContainer}>
|
<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}>
|
<div className={styles.gameOptionHeader}>
|
||||||
<h2>{t("executable_section_title")}</h2>
|
<h2>{t("executable_section_title")}</h2>
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
downloadSlice,
|
downloadSlice,
|
||||||
windowSlice,
|
windowSlice,
|
||||||
librarySlice,
|
librarySlice,
|
||||||
|
collectionsSlice,
|
||||||
searchSlice,
|
searchSlice,
|
||||||
userPreferencesSlice,
|
userPreferencesSlice,
|
||||||
toastSlice,
|
toastSlice,
|
||||||
|
@ -15,6 +16,7 @@ export const store = configureStore({
|
||||||
search: searchSlice.reducer,
|
search: searchSlice.reducer,
|
||||||
window: windowSlice.reducer,
|
window: windowSlice.reducer,
|
||||||
library: librarySlice.reducer,
|
library: librarySlice.reducer,
|
||||||
|
collections: collectionsSlice.reducer,
|
||||||
userPreferences: userPreferencesSlice.reducer,
|
userPreferences: userPreferencesSlice.reducer,
|
||||||
download: downloadSlice.reducer,
|
download: downloadSlice.reducer,
|
||||||
toast: toastSlice.reducer,
|
toast: toastSlice.reducer,
|
||||||
|
|
|
@ -128,12 +128,19 @@ export interface Game {
|
||||||
objectID: string;
|
objectID: string;
|
||||||
shop: GameShop;
|
shop: GameShop;
|
||||||
downloadQueue: DownloadQueue | null;
|
downloadQueue: DownloadQueue | null;
|
||||||
|
collections: Collection[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LibraryGame = Omit<Game, "repacks">;
|
export type LibraryGame = Omit<Game, "repacks">;
|
||||||
|
|
||||||
|
export interface Collection {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
games: Game[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface GameRunning {
|
export interface GameRunning {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue