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 custom user-defined game categories #1428 #3115

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
82ef59c
feat: created basic POC of this feature
lgcavalheiro Oct 4, 2023
bda5e12
chore: merge with main
lgcavalheiro Oct 4, 2023
b4d82d0
feat: added dropdown selector for custom categories
lgcavalheiro Oct 5, 2023
b6eb420
feat: added translations
lgcavalheiro Oct 5, 2023
dfa1582
feat: added translations
lgcavalheiro Oct 5, 2023
a1e0c05
fix: fixed issues raised during review + added uncategorized preset
lgcavalheiro Oct 7, 2023
1459ce7
chore: pushing en translation files
lgcavalheiro Oct 7, 2023
33d30fa
chore: rebase against upstream/main + conflict resolution
lgcavalheiro Nov 9, 2023
a35d9ea
fix: runner is now relevant + added validation for new categories
lgcavalheiro Nov 10, 2023
eeb61bb
fix: changed category settings layout to favor checkboxes
lgcavalheiro Nov 12, 2023
9a97fec
Group library filters in a dropdown. Add only installed filter
arielj Nov 25, 2023
ef463b5
Favorites filter composable. Refresh libraries in background
arielj Nov 25, 2023
398c2f8
Re-add translation strings
arielj Nov 25, 2023
cb452a8
Fix checked status for store filters
arielj Nov 25, 2023
3eda2ee
Remove console.log
arielj Nov 25, 2023
ba930a2
fix: category filter on small screens
lgcavalheiro Nov 26, 2023
86d1dd4
Fix missing windows games when no platform is selected
arielj Nov 28, 2023
1b0f265
fix: merge with upstream/main + added a gap between catagories select…
lgcavalheiro Nov 29, 2023
cbf185a
fix: i18n errors on push
lgcavalheiro Nov 29, 2023
637ce96
chore: more merges with upstream/main
lgcavalheiro Nov 30, 2023
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
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