Skip to content

Commit

Permalink
[Feature] Add custom user-defined game categories #1428 (#3115)
Browse files Browse the repository at this point in the history
* feat: created basic POC of this feature

* feat: added dropdown selector for custom categories

* feat: added translations

* feat: added translations

* fix: fixed issues raised during review + added uncategorized preset

* chore: pushing en translation files

* fix: runner is now relevant + added validation for new categories

* fix: changed category settings layout to favor checkboxes

* Group library filters in a dropdown. Add only installed filter

* Favorites filter composable. Refresh libraries in background

* Re-add translation strings

* Fix checked status for store filters

* Remove console.log

* fix: category filter on small screens

* Fix missing windows games when no platform is selected

* fix: i18n errors on push

---------

Co-authored-by: Ariel Juodziukynas <arieljuod@gmail.com>
  • Loading branch information
lgcavalheiro and arielj authored Nov 30, 2023
1 parent f0cd114 commit b6281f9
Show file tree
Hide file tree
Showing 18 changed files with 417 additions and 40 deletions.
1 change: 1 addition & 0 deletions public/locales/en/gamepage.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@
"submenu": {
"addShortcut": "Add shortcut",
"addToSteam": "Add to Steam",
"categories": "Categories",
"change": "Change install path",
"disableEosOverlay": "Disable EOS Overlay",
"enableEosOverlay": "Enable EOS Overlay",
Expand Down
12 changes: 11 additions & 1 deletion public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@
"go_to_library": "Go to Library",
"login": "Log in"
},
"category-settings": {
"add-new-category": "Add New Category",
"cancel": "Cancel",
"delete-question": "Proceeding will permanently remove this category and unassign it from all games. Continue?",
"new-category": "New Category",
"remove-category": "Remove Category",
"warning": "Warning"
},
"controller": {
"hints": {
"back": "Back",
Expand Down Expand Up @@ -245,11 +253,13 @@
"GOG": "GOG",
"gog-store": "GOG Store",
"header": {
"all_categories": "All Categories",
"filters": "Filters",
"show_available_games": "Show non-Available games",
"show_favourites_only": "Show Favourites only",
"show_hidden": "Show Hidden",
"show_installed_only": "Show Installed only"
"show_installed_only": "Show Installed only",
"uncategorized": "Uncategorized"
},
"help": {
"amdfsr": "AMD's FSR helps boost framerate by upscaling lower resolutions in Fullscreen Mode. Image quality increases from 5 to 1 at the cost of a slight performance hit. Enabling may improve performance.",
Expand Down
1 change: 1 addition & 0 deletions src/common/types/electron_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface StoreStructure {
recent: RecentGame[]
hidden: HiddenGame[]
favourites: FavouriteGame[]
customCategories: Record<string, string[]>
}
theme: string
zoomPercent: number
Expand Down
9 changes: 9 additions & 0 deletions src/frontend/components/UI/CategoryFilter/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#custom-category-selector {
width: 324px;
}

@media screen and (max-width: 1000px) {
#custom-category-selector {
width: auto;
}
}
31 changes: 31 additions & 0 deletions src/frontend/components/UI/CategoryFilter/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import SelectField from '../SelectField'
import ContextProvider from 'frontend/state/ContextProvider'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import './index.css'

export default function CategoryFilter() {
const { customCategories, currentCustomCategory, setCurrentCustomCategory } =
useContext(ContextProvider)
const { t } = useTranslation()

return (
<SelectField
htmlId="custom-category-selector"
value={currentCustomCategory || ''}
onChange={(e) => {
setCurrentCustomCategory(e.target.value)
}}
>
<option value="">{t('header.all_categories', 'All Categories')}</option>
<option value="preset_uncategorized">
{t('header.uncategorized', 'Uncategorized')}
</option>
{customCategories.listCategories().map((category) => (
<option value={category} key={category}>
{category}
</option>
))}
</SelectField>
)
}
1 change: 1 addition & 0 deletions src/frontend/components/UI/Header/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
display: flex;
align-self: center;
justify-self: flex-end;
gap: 1em;
}

.Header__filters .FormControl {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/components/UI/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import LibrarySearchBar from '../LibrarySearchBar'
import CategoryFilter from '../CategoryFilter'
import LibraryFilters from '../LibraryFilters'
import './index.css'

Expand All @@ -11,6 +12,7 @@ export default function Header() {
<LibrarySearchBar />
</div>
<span className="Header__filters">
<CategoryFilter />
<LibraryFilters />
</span>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/frontend/screens/Game/GamePage/components/DotsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const DotsMenu = ({ gameInfo, handleUpdate }: Props) => {
hasRequirements ? () => setShowRequirements(true) : undefined
}
onShowDlcs={() => setShowDlcs(true)}
gameInfo={gameInfo}
/>
</div>

Expand Down
4 changes: 2 additions & 2 deletions src/frontend/screens/Game/GamePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ export default React.memo(function GamePage(): JSX.Element | null {
platform,
showDialogModal,
isSettingsModalOpen,
experimentalFeatures,
connectivity
connectivity,
experimentalFeatures
} = useContext(ContextProvider)

const [gameInfo, setGameInfo] = useState(locationGameInfo)
Expand Down
21 changes: 17 additions & 4 deletions src/frontend/screens/Game/GameSubMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import './index.css'

import React, { useContext, useEffect, useState } from 'react'

import { GameStatus, Runner, WikiInfo } from 'common/types'
import { GameInfo, GameStatus, Runner, WikiInfo } from 'common/types'

import { createNewWindow, repair } from 'frontend/helpers'
import { useTranslation } from 'react-i18next'
Expand All @@ -23,6 +23,7 @@ interface Props {
disableUpdate: boolean
onShowRequirements?: () => void
onShowDlcs?: () => void
gameInfo: GameInfo
}

export default function GamesSubmenu({
Expand All @@ -34,10 +35,16 @@ export default function GamesSubmenu({
handleUpdate,
disableUpdate,
onShowRequirements,
onShowDlcs
onShowDlcs,
gameInfo
}: Props) {
const { refresh, platform, libraryStatus, showDialogModal } =
useContext(ContextProvider)
const {
refresh,
platform,
libraryStatus,
showDialogModal,
setIsSettingsModalOpen
} = useContext(ContextProvider)
const isWin = platform === 'win32'
const isLinux = platform === 'linux'

Expand Down Expand Up @@ -314,6 +321,12 @@ export default function GamesSubmenu({
))}
</>
)}
<button
onClick={() => setIsSettingsModalOpen(true, 'category', gameInfo)}
className="link button is-text is-link"
>
{t('submenu.categories', 'Categories')}
</button>
{!isSideloaded && storeUrl && (
<NavLink
className="link button is-text is-link"
Expand Down
5 changes: 5 additions & 0 deletions src/frontend/screens/Library/components/GameCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,11 @@ const GameCard = ({
onclick: () => favouriteGames.add(appName, title),
show: !isFavouriteGame
},
{
label: t('submenu.categories', 'Categories'),
onclick: () => setIsSettingsModalOpen(true, 'category', gameInfo),
show: true
},
{
label: t('button.remove_from_favourites', 'Remove From Favourites'),
onclick: () => favouriteGames.remove(appName),
Expand Down
70 changes: 48 additions & 22 deletions src/frontend/screens/Library/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ export default React.memo(function Library(): JSX.Element {
sideloadedLibrary,
favouriteGames,
libraryTopSection,
hiddenGames,
platform
platform,
currentCustomCategory,
customCategories,
hiddenGames
} = useContext(ContextProvider)

const [layout, setLayout] = useState(storage.getItem('layout') || 'grid')
Expand Down Expand Up @@ -321,10 +323,7 @@ export default React.memo(function Library(): JSX.Element {
return favourites.map((game) => `${game.app_name}_${game.runner}`)
}, [favourites])

// select library
const libraryToShow = useMemo(() => {
let library: Array<GameInfo> = []

const makeLibrary = () => {
let displayedStores: string[] = []
if (storesFilters['gog'] && gog.username) {
displayedStores.push('gog')
Expand Down Expand Up @@ -353,29 +352,56 @@ export default React.memo(function Library(): JSX.Element {
const sideloadedApps = showSideloaded ? sideloadedLibrary : []
const amazonLibrary = showAmazon ? amazon.library : []

library = [
...sideloadedApps,
...epicLibrary,
...gogLibrary,
...amazonLibrary
]
return [...sideloadedApps, ...epicLibrary, ...gogLibrary, ...amazonLibrary]
}

// select library
const libraryToShow = useMemo(() => {
let library: Array<GameInfo> = makeLibrary()

if (showFavouritesLibrary) {
library = library.filter((game) =>
favouritesIds.includes(`${game.app_name}_${game.runner}`)
)
}
} else if (currentCustomCategory && currentCustomCategory.length > 0) {
if (currentCustomCategory === 'preset_uncategorized') {
// list of all games that have at least one category assigned to them
const categorizedGames = Array.from(
new Set(Object.values(customCategories.list).flat())
)

library = library.filter(
(game) =>
!categorizedGames.includes(`${game.app_name}_${game.runner}`)
)
} else {
const gamesInCustomCategory =
customCategories.list[currentCustomCategory]

if (showInstalledOnly) {
library = library.filter((game) => game.is_installed)
}
library = library.filter((game) =>
gamesInCustomCategory.includes(`${game.app_name}_${game.runner}`)
)
}
} else {
if (!showNonAvailable) {
const nonAvailbleGames = storage.getItem('nonAvailableGames') || '[]'
const nonAvailbleGamesArray = JSON.parse(nonAvailbleGames)
library = library.filter(
(game) => !nonAvailbleGamesArray.includes(game.app_name)
)
}

if (!showNonAvailable) {
const nonAvailbleGames = storage.getItem('nonAvailableGames') || '[]'
const nonAvailbleGamesArray = JSON.parse(nonAvailbleGames)
library = library.filter(
(game) => !nonAvailbleGamesArray.includes(game.app_name)
)
if (showInstalledOnly) {
library = library.filter((game) => game.is_installed)
}

if (!showNonAvailable) {
const nonAvailbleGames = storage.getItem('nonAvailableGames') || '[]'
const nonAvailbleGamesArray = JSON.parse(nonAvailbleGames)
library = library.filter(
(game) => !nonAvailbleGamesArray.includes(game.app_name)
)
}
}

// filter
Expand Down
25 changes: 17 additions & 8 deletions src/frontend/screens/Settings/components/SettingsModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext } from 'react'
import React, { useContext, useMemo } from 'react'
import { GameInfo } from 'common/types'
import {
Dialog,
Expand All @@ -13,10 +13,11 @@ import LogSettings from '../../sections/LogSettings'
import './index.scss'
import { useTranslation } from 'react-i18next'
import { SettingsContextType } from 'frontend/types'
import CategorySettings from '../../sections/CategorySettings'

type Props = {
gameInfo: GameInfo
type: 'settings' | 'log'
type: 'settings' | 'log' | 'category'
}

function SettingsModal({ gameInfo, type }: Props) {
Expand All @@ -32,6 +33,16 @@ function SettingsModal({ gameInfo, type }: Props) {
runner
})

const titleType = useMemo(() => {
const titleTypeLiterals = {
settings: t('Settings', 'Settings'),
log: t('settings.navbar.log', 'Log'),
category: 'Categories'
}

return titleTypeLiterals[type]
}, [type])

if (!contextValues) {
return null
}
Expand All @@ -43,15 +54,13 @@ function SettingsModal({ gameInfo, type }: Props) {
className={'InstallModal__dialog'}
>
<DialogHeader onClose={() => setIsSettingsModalOpen(false)}>
{`${title} (${
type === 'settings'
? t('Settings', 'Settings')
: t('settings.navbar.log', 'Log')
})`}
{`${title} (${titleType})`}
</DialogHeader>
<DialogContent className="settingsDialogContent">
<SettingsContext.Provider value={contextValues}>
{type === 'settings' ? <GamesSettings /> : <LogSettings />}
{type === 'settings' && <GamesSettings />}
{type === 'log' && <LogSettings />}
{type === 'category' && <CategorySettings />}
</SettingsContext.Provider>
</DialogContent>
</Dialog>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.NewCategoryInput {
padding: 0 !important;

> input[type='text'] {
height: 53px;
}
}
Loading

0 comments on commit b6281f9

Please sign in to comment.