Skip to content

[UI/Fix] Categories improvements: make them composable, update style, fix combination with filters #3303

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

Merged
merged 7 commits into from
Dec 10, 2023
Merged
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
4 changes: 3 additions & 1 deletion public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,12 @@
"GOG": "GOG",
"gog-store": "GOG Store",
"header": {
"all_categories": "All Categories",
"categories": "Categories",
"filters": "Filters",
"no_categories": "No custom categories. Add categories using each game menu.",
"only": "only",
"reset": "Reset",
"select_all": "Select All",
"show_available_games": "Show non-Available games",
"show_favourites_only": "Show Favourites only",
"show_hidden": "Show Hidden",
Expand Down
9 changes: 0 additions & 9 deletions src/frontend/components/UI/CategoryFilter/index.css

This file was deleted.

119 changes: 97 additions & 22 deletions src/frontend/components/UI/CategoryFilter/index.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,106 @@
import SelectField from '../SelectField'
import ContextProvider from 'frontend/state/ContextProvider'
import React, { useContext } from 'react'
import ContextProvider from 'frontend/state/ContextProvider'
import { useTranslation } from 'react-i18next'
import './index.css'
import ToggleSwitch from '../ToggleSwitch'

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

const toggleCategory = (category: string) => {
if (currentCustomCategories.includes(category)) {
const newCategories = currentCustomCategories.filter(
(cat) => cat !== category
)
setCurrentCustomCategories(newCategories)
} else {
setCurrentCustomCategories([...currentCustomCategories, category])
}
}

const setCategoryOnly = (category: string) => {
setCurrentCustomCategories([category])
}

const selectAll = () => {
setCurrentCustomCategories(
['preset_uncategorized'].concat(customCategories.listCategories())
)
}

const toggleWithOnly = (
toggle: JSX.Element,
onOnlyClicked: () => void,
category: string
) => {
return (
<div className="toggleWithOnly" key={category}>
{toggle}
<button className="only" onClick={() => onOnlyClicked()}>
{t('header.only', 'only')}
</button>
</div>
)
}

const categoryToggle = (categoryName: string, categoryValue?: string) => {
const toggle = (
<ToggleSwitch
htmlId={categoryValue || categoryName}
handleChange={() => toggleCategory(categoryValue || categoryName)}
value={currentCustomCategories.includes(categoryValue || categoryName)}
title={categoryName}
/>
)

const onOnlyClick = () => {
setCategoryOnly(categoryValue || categoryName)
}

return toggleWithOnly(toggle, onOnlyClick, categoryValue || categoryName)
}

let dropdownContent = (
<span>
{t(
'header.no_categories',
'No custom categories. Add categories using each game menu.'
)}
</span>
)
const categoriesList = customCategories.listCategories()

if (categoriesList.length > 0) {
dropdownContent = (
<>
{categoriesList.map((category) => categoryToggle(category))}
<hr />
{categoryToggle(
t('header.uncategorized', 'Uncategorized'),
'preset_uncategorized'
)}
<hr />
<button
type="reset"
className="button is-primary"
onClick={() => selectAll()}
>
{t('header.select_all', 'Select All')}
</button>
</>
)
}

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>
<div className="categoriesFilter">
<button className="selectStyle">
{t('header.categories', 'Categories')}
</button>
<div className="dropdown">{dropdownContent}</div>
</div>
)
}
4 changes: 3 additions & 1 deletion src/frontend/components/UI/LibraryFilters/index.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.libraryFilters {
.libraryFilters,
.categoriesFilter {
position: relative;

.button {
Expand All @@ -8,6 +9,7 @@
position: absolute;
top: 100%;
right: 0px;
min-width: 250px;
display: flex;
flex-direction: column;
background: var(--body-background);
Expand Down
48 changes: 32 additions & 16 deletions src/frontend/screens/Library/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default React.memo(function Library(): JSX.Element {
favouriteGames,
libraryTopSection,
platform,
currentCustomCategory,
currentCustomCategories,
customCategories,
hiddenGames
} = useContext(ContextProvider)
Expand Down Expand Up @@ -363,26 +363,42 @@ export default React.memo(function Library(): JSX.Element {
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())
)
} else {
if (currentCustomCategories && currentCustomCategories.length > 0) {
const gamesInSelectedCategories = new Set<string>()

// loop through selected categories and add all games in all those categories
currentCustomCategories.forEach((category) => {
if (category === 'preset_uncategorized') {
// in the case of the special "uncategorized" category, we read all
// the categorized games and add the others to the list to show
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]
library.forEach((game) => {
if (
!categorizedGames.includes(`${game.app_name}_${game.runner}`)
) {
gamesInSelectedCategories.add(`${game.app_name}_${game.runner}`)
}
})
} else {
const gamesInCustomCategory = customCategories.list[category]

if (gamesInCustomCategory) {
gamesInCustomCategory.forEach((game) => {
gamesInSelectedCategories.add(game)
})
}
}
})

library = library.filter((game) =>
gamesInCustomCategory.includes(`${game.app_name}_${game.runner}`)
gamesInSelectedCategories.has(`${game.app_name}_${game.runner}`)
)
}
} else {

if (!showNonAvailable) {
const nonAvailbleGames = storage.getItem('nonAvailableGames') || '[]'
const nonAvailbleGamesArray = JSON.parse(nonAvailbleGames)
Expand Down
13 changes: 10 additions & 3 deletions src/frontend/screens/Settings/sections/CategorySettings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import {
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'

const CategorySettings = () => {
const { customCategories, currentCustomCategory, setCurrentCustomCategory } =
useContext(ContextProvider)
const {
customCategories,
currentCustomCategories,
setCurrentCustomCategories
} = useContext(ContextProvider)
const { appName, runner } = useContext(SettingsContext)

const [newCategory, setNewCategory] = useState('')
Expand Down Expand Up @@ -68,7 +71,11 @@ const CategorySettings = () => {
}

const handleRemoveCategory = (category: string) => {
if (currentCustomCategory === category) setCurrentCustomCategory('')
if (
currentCustomCategories.length === 1 &&
currentCustomCategories[0] === category
)
setCurrentCustomCategories([])
customCategories.removeCategory(category)
updateCategories()
setCategoryToDelete('')
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/state/ContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ const initialContext: ContextType = {
add: () => null,
remove: () => null
},
currentCustomCategory: null,
setCurrentCustomCategory: () => null,
currentCustomCategories: [],
setCurrentCustomCategories: () => null,
favouriteGames: {
list: [],
add: () => null,
Expand Down
39 changes: 33 additions & 6 deletions src/frontend/state/GlobalState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ interface StateProps {
hiddenGames: HiddenGame[]
favouriteGames: FavouriteGame[]
customCategories: Record<string, string[]>
currentCustomCategory: string | null
currentCustomCategories: string[]
theme: string
isFullscreen: boolean
isFrameless: boolean
Expand Down Expand Up @@ -110,6 +110,21 @@ interface StateProps {
experimentalFeatures: ExperimentalFeatures
}

// function to load the new key or fallback to the old one
const loadCurrentCategories = () => {
const currentCategories = storage.getItem('current_custom_categories') || null
if (!currentCategories) {
const currentCategory = storage.getItem('current_custom_category') || null
if (!currentCategory) {
return []
} else {
return [currentCategory]
}
} else {
return JSON.parse(currentCategories) as string[]
}
}

class GlobalState extends PureComponent<Props> {
loadGOGLibrary = (): Array<GameInfo> => {
const games = gogLibraryStore.get('games', [])
Expand Down Expand Up @@ -155,7 +170,7 @@ class GlobalState extends PureComponent<Props> {
refreshing: false,
refreshingInTheBackground: true,
hiddenGames: configStore.get('games.hidden', []),
currentCustomCategory: storage.getItem('current_custom_category') || null,
currentCustomCategories: loadCurrentCategories(),
sidebarCollapsed: JSON.parse(
storage.getItem('sidebar_collapsed') || 'false'
),
Expand Down Expand Up @@ -195,8 +210,12 @@ class GlobalState extends PureComponent<Props> {
}
}

setCurrentCustomCategory = (newCustomCategory: string) => {
this.setState({ currentCustomCategory: newCustomCategory })
setCurrentCustomCategories = (newCustomCategories: string[]) => {
storage.setItem(
'current_custom_categories',
JSON.stringify(newCustomCategories)
)
this.setState({ currentCustomCategories: newCustomCategories })
}

setLanguage = (newLanguage: string) => {
Expand Down Expand Up @@ -309,8 +328,16 @@ class GlobalState extends PureComponent<Props> {
const newCustomCategories = this.state.customCategories
newCustomCategories[newCategory] = []

// when adding a new category, if there are categories selected, select the new
// one too so the game doesn't disappear form the library
let newCurrentCustomCategories = this.state.currentCustomCategories
if (this.state.currentCustomCategories.length > 0) {
newCurrentCustomCategories = [...newCurrentCustomCategories, newCategory]
}

this.setState({
customCategories: newCustomCategories
customCategories: newCustomCategories,
currentCustomCategories: newCurrentCustomCategories
})
configStore.set('games.customCategories', newCustomCategories)
}
Expand Down Expand Up @@ -957,7 +984,7 @@ class GlobalState extends PureComponent<Props> {
setLastChangelogShown: this.setLastChangelogShown,
isSettingsModalOpen: settingsModalOpen,
setIsSettingsModalOpen: this.handleSettingsModalOpen,
setCurrentCustomCategory: this.setCurrentCustomCategory
setCurrentCustomCategories: this.setCurrentCustomCategories
}}
>
{this.props.children}
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ export interface ContextType {
addCategory: (newCategory: string) => void
removeCategory: (category: string) => void
}
currentCustomCategory: string | null
setCurrentCustomCategory: (newCustomCategory: string) => void
currentCustomCategories: string[]
setCurrentCustomCategories: (newCustomCategories: string[]) => void
theme: string
setTheme: (themeName: string) => void
zoomPercent: number
Expand Down