Skip to content

Commit

Permalink
[UI/Fix] Categories improvements: make them composable, update style,…
Browse files Browse the repository at this point in the history
… fix combination with filters (#3303)

* Add an 'only' action to select one store/platform only

* Only show 'only' action when hovering/focusing a filter

* Filters: reset button, select style

* Update locale

* Categories improvements: compose categories, update style, fix issue with other filters

* Fix i18next
  • Loading branch information
arielj authored Dec 10, 2023
1 parent 612566c commit 408ff9f
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 62 deletions.
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

0 comments on commit 408ff9f

Please sign in to comment.