Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add collections section #793

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 13 additions & 0 deletions src/locales/ru/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,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}} готова к установке",
Expand Down
2 changes: 2 additions & 0 deletions src/main/data-source.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DataSource } from "typeorm";
import {
Collection,
DownloadQueue,
DownloadSource,
Game,
Expand All @@ -19,6 +20,7 @@ export const createDataSource = (
new DataSource({
type: "better-sqlite3",
entities: [
Collection,
Game,
Repack,
UserPreferences,
Expand Down
21 changes: 21 additions & 0 deletions src/main/entity/collection.entity.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
1 change: 1 addition & 0 deletions src/main/entity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 18 additions & 0 deletions src/main/events/collections/add-collection-game.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions src/main/events/collections/add-collection.ts
Original file line number Diff line number Diff line change
@@ -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);
19 changes: 19 additions & 0 deletions src/main/events/collections/get-collections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { collectionRepository } from "@main/repository";
import { registerEvent } from "../register-event";

const getCollections = async () =>
collectionRepository.find({
relations: {
games: true,
},
select: {
games: {
id: true,
},
},
order: {
title: "asc",
},
});

registerEvent("getCollections", getCollections);
18 changes: 18 additions & 0 deletions src/main/events/collections/remove-collection-game.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions src/main/events/collections/remove-collection.ts
Original file line number Diff line number Diff line change
@@ -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);
5 changes: 5 additions & 0 deletions src/main/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
3 changes: 3 additions & 0 deletions src/main/repository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { dataSource } from "./data-source";
import {
Collection,
DownloadQueue,
DownloadSource,
Game,
Expand All @@ -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);
12 changes: 12 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type {
AppUpdaterEvent,
StartGameDownloadPayload,
GameRunning,
Collection,
Game,
FriendRequestAction,
} from "@types";

Expand Down Expand Up @@ -103,6 +105,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),
Expand Down
81 changes: 73 additions & 8 deletions src/renderer/src/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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[]>([]);
Expand All @@ -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();

Expand All @@ -46,7 +49,8 @@ export function Sidebar() {

useEffect(() => {
updateLibrary();
}, [lastPacket?.game.id, updateLibrary]);
updateCollections();
}, [lastPacket?.game.id, updateLibrary, updateCollections]);

const isDownloading = sortedLibrary.some(
(game) => game.status === "active" && game.progress !== 1
Expand All @@ -67,18 +71,27 @@ 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);
}, [sortedLibrary]);
setFilteredLibrary(
sortedLibrary.filter(
(game) =>
!collections.some((collection) =>
collection.games.some(
(collectionGame) => collectionGame.id == game.id
)
)
)
);
}, [sortedLibrary, collections]);

useEffect(() => {
window.onmousemove = (event: MouseEvent) => {
Expand Down Expand Up @@ -199,6 +212,58 @@ 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}>
{sortedLibrary
.filter((game) =>
collection.games.some(
(collectionGame) => game.id == collectionGame.id
)
)
.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
Expand Down
8 changes: 8 additions & 0 deletions src/renderer/src/declaration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
RealDebridUser,
DownloadSource,
UserProfile,
Collection,
FriendRequest,
FriendRequestAction,
} from "@types";
Expand Down Expand Up @@ -80,6 +81,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: (
Expand Down
26 changes: 26 additions & 0 deletions src/renderer/src/features/collections-slice.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading