mirror of
https://github.com/RetroDECK/ES-DE.git
synced 2024-11-22 14:15:38 +00:00
Merge branch 'master' into 575-theme-add-a-modern-clean-switch-like-theme-as-an-official-theme-in-es-de-to-choose-from
# Conflicts: # es-app/src/views/gamelist/DetailedGameListView.cpp # es-app/src/views/gamelist/GridGameListView.cpp # es-app/src/views/gamelist/VideoGameListView.cpp
This commit is contained in:
commit
54e54161d1
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -13,8 +13,10 @@
|
|||
* Added alternative emulators support where additional emulators can be defined in es_systems.xml and be selected system-wide or per game via the user interface
|
||||
* Populated the bundled es_systems.xml files with alternative emulator entries for most RetroArch cores
|
||||
* Added a virtual keyboard, partly based on code from batocera-emulationstation
|
||||
* Added badges that indicate favorite/completed/broken games as well as games suitable for children and those with a selected alternative emulator
|
||||
* Added the ability to make complementary game system customizations without having to replace the entire bundled es_systems.xml file
|
||||
* Added support for an optional \<systemsortname\> tag for es_systems.xml that can be used to override the default \<fullname\> systems sorting
|
||||
* Added menu scroll indicators showing if there are additional entries available below or above what's currently shown on screen
|
||||
* Improved the gamelist filter screen to not allow filtering of values where there is no actual data to filter, e.g. Favorites for a system with no favorite games
|
||||
* Grayed out all fields in the gamelist filter screen where there is no data to filter, previously some fields were removed entirely and some could still be used
|
||||
* Added the ability to filter on blank/unknown values for Genre, Player, Developer, Publisher and Alternative emulator.
|
||||
|
@ -25,6 +27,7 @@
|
|||
* Made the scrolling speed of ScrollableContainer more consistent across various screen resolutions and display aspect ratios
|
||||
* Made the game name and description stop scrolling when running the media viewer, the screensaver or when running in the background while a game is launched
|
||||
* Added notification popups when plugging in or removing controllers
|
||||
* Changed to loading the default theme set rbsimple-DE instead of the first available theme if the currently configured theme is missing
|
||||
* Added support for using the left and right trigger buttons in the help prompts
|
||||
* Removed the "Choose" entry from the help prompts in the gamelist view
|
||||
* Changed the "Toggle screensaver" help entry in the system view to simply "Screensaver"
|
||||
|
@ -35,9 +38,11 @@
|
|||
* Added a blinking cursor to TextEditComponent
|
||||
* Changed the filter description "Text filter (game name)" to "Game name"
|
||||
* Added support for multi-select total count and exclusive multi-select to OptionListComponent
|
||||
* Added support for a maximum name length to OptionListComponent (non-multiselect only) with an abbreviation of the name if it exceeds this value
|
||||
* Added support for key repeat to OptionListComponent, making it possible to cycle through the options by holding the left or right button
|
||||
* Added key repeat for the "Jump to" and "Sort games by" selectors on the game options menu
|
||||
* Added key repeat when editing the "Release date" entry in the metadata editor (DateTimeEditComponent)
|
||||
* Added support for setting the Kidgame metadata flag for folders (which will only affect the badges)
|
||||
* Achieved a massive speed improvement for OptionListComponent by not resizing each added MenuComponent row (most notable in the filter GUI)
|
||||
* Made multiple optimizations to the GUI components by removing lots of unnecessary function calls for sizing, placement, opacity changes etc.
|
||||
* Simplified the logic for info popups and prepared the code for the future "multiple popups" feature
|
||||
|
@ -74,14 +79,19 @@
|
|||
* Leading and trailing whitespace characters would get included in scraper search refines and TheGamesDB searches
|
||||
* Game name (text) filters were matching the system names for collection systems if the "Show system names in collections" setting was enabled
|
||||
* Brackets such as () and [] were filtered from game names in collection systems if the "Show system names in collections" setting was enabled
|
||||
* Fixed multiple issues where ComponentGrid would display incorrect help prompts
|
||||
* Help prompts were missing for the "Rating" and "Release date" fields in the metadata editor
|
||||
* There was some strange behavior in DateTimeEditComponent when changing the date all the way down to 1970-01-01
|
||||
* When navigating menus, the separator lines and menu components did not align properly and moved up and down slightly
|
||||
* Under some circumstances and at some screen resolutions, the last menu separator line would not get rendered (still an issue at extreme resolutions like 320x240)
|
||||
* When scrolling in menus, pressing other buttons than "Up" or "Down" did not stop the scrolling which caused all sorts of weird behavior
|
||||
* With the menu scale-up effect enabled and entering a submenu before the parent menu was completely scaled up, the parent would get stuck at a semi-scaled size
|
||||
* If there was an abbreviated full system name for the "Gamelist on startup" option, that abbreviation would also get displayed when opening the selector window
|
||||
* Really long theme set names would not get abbreviated in the UI settings menu, leading to a garbled "Theme set" setting row
|
||||
* Disabling a collection while its gamelist was displayed would lead to a slide transition from a black screen if a gamelist on startup had been set
|
||||
* When marking a game to not be counted in the metadata editor and the game was part of a custom collection, no collection disabling notification was displayed
|
||||
* Horizontal sizing of the TextComponent input field was not consistent across different screen resolutions
|
||||
* The sizing of the metadata editor was strange, which was clearly visible when activating the Ctrl+G debug mode
|
||||
* The "sortname" window header was incorrectly spelled when editing this type of entry in the metadata editor
|
||||
* When the last row of a menu had its text color changed, this color was completely desaturated when navigating to a button below the list
|
||||
|
||||
|
|
|
@ -30,7 +30,6 @@ This plan is under constant review so expect it to change from time to time. Sti
|
|||
|
||||
* Support for pre-defined alternative emulators and cores (configured in es_systems.xml)
|
||||
* Badges highlighting things like favorite games, completed games etc. (will require theme support)
|
||||
* Improved full-screen support, removing the temporary full-screen hacks
|
||||
* Virtual (on-screen) keyboard
|
||||
* Support for the Raspberry Pi 4 (Raspberry Pi OS)
|
||||
* Add GLM library dependency for matrix and vector operations, decommission the built-in functions
|
||||
|
@ -39,21 +38,22 @@ This plan is under constant review so expect it to change from time to time. Sti
|
|||
|
||||
#### v1.3
|
||||
|
||||
* Localization/multi-language support
|
||||
* New theme engine with generalized views (only System and Gamelist) and theme variants support
|
||||
* Add multiple new gamelist components (wheels, wall/grid etc.)
|
||||
* Move existing theme logic to legacy support, only to be used for backwards compatibility
|
||||
* Improve full-screen support and make game launching more seamless, remove the temporary full-screen hacks
|
||||
* Checksum support for the scraper for exact searches and for determining when to overwrite files
|
||||
* Improved text and font functions, e.g. faster and cleaner line wrapping and more exact sizing
|
||||
* Improve text and font functions, e.g. faster and cleaner line wrapping and more exact sizing
|
||||
|
||||
#### v1.4
|
||||
|
||||
* Localization/multi-language support
|
||||
* Authoring tools to clean up orphaned gamelist entries, media files etc.
|
||||
* Scrollbar component for the gamelist view which can be used by the themes
|
||||
* Web proxy support for the scraper
|
||||
* Add "time played" counter per game, similar to how it works in Steam
|
||||
* Preload all built-in resources and never clear them from the cache
|
||||
* Improved multi-threading
|
||||
* Improve multi-threading
|
||||
|
||||
#### v1.5
|
||||
|
||||
|
|
|
@ -368,6 +368,8 @@ Below are the default zIndex values per element type:
|
|||
* `text name="logoPlaceholderText"`
|
||||
* Gamelist information - 50
|
||||
* `text name="gamelistInfo"`
|
||||
* Badges - 50
|
||||
* `badges name="md_badges"`
|
||||
|
||||
### Theme variables
|
||||
|
||||
|
@ -486,7 +488,7 @@ or to specify only a portion of the value of a theme property:
|
|||
* `text name="md_players"` - ALL
|
||||
- The "players" metadata (number of players the game supports).
|
||||
* `badges name="md_badges"` - ALL
|
||||
- The "badges" metadata. Displayed as a group of badges that indicate boolean metadata such as favorite, broken.
|
||||
- The "badges" metadata. Displayed as a group of badges that indicate metadata such as favorites and completed games.
|
||||
* `datetime name="md_lastplayed"` - ALL
|
||||
- The "lastplayed" metadata. Displayed as a string representing the time relative to "now" (e.g. "3 hours ago").
|
||||
* `text name="md_playcount"` - ALL
|
||||
|
@ -543,7 +545,7 @@ or to specify only a portion of the value of a theme property:
|
|||
* `text name="md_players"` - ALL
|
||||
- The "players" metadata (number of players the game supports).
|
||||
* `badges name="md_badges"` - ALL
|
||||
- The "badges" metadata. Displayed as a group of badges that indicate boolean metadata such as favorite, broken.
|
||||
- The "badges" metadata. Displayed as a group of badges that indicate metadata such as favorites and completed games.
|
||||
* `datetime name="md_lastplayed"` - ALL
|
||||
- The "lastplayed" metadata. Displayed as a string representing the time relative to "now" (e.g. "3 hours ago").
|
||||
* `text name="md_playcount"` - ALL
|
||||
|
@ -598,7 +600,7 @@ or to specify only a portion of the value of a theme property:
|
|||
* `text name="md_players"` - ALL
|
||||
- The "players" metadata (number of players the game supports).
|
||||
* `badges name="md_badges"` - ALL
|
||||
- The "badges" metadata. Displayed as a group of badges that indicate boolean metadata such as favorite, broken.
|
||||
- The "badges" metadata. Displayed as a group of badges that indicate metadata such as favorites and completed games.
|
||||
* `datetime name="md_lastplayed"` - ALL
|
||||
- The "lastplayed" metadata. Displayed as a string representing the time relative to "now" (e.g. "3 hours ago").
|
||||
* `text name="md_playcount"` - ALL
|
||||
|
@ -642,7 +644,7 @@ Can be created as an extra.
|
|||
* `rotation` - type: FLOAT.
|
||||
- angle in degrees that the image should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise.
|
||||
* `rotationOrigin` - type: NORMALIZED_PAIR.
|
||||
- Point around which the image will be rotated. Defaults to `0.5 0.5`.
|
||||
- Point around which the image will be rotated. Default is `0.5 0.5`.
|
||||
* `path` - type: PATH.
|
||||
- Path to the image file. Most common extensions are supported (including .jpg, .png, and unanimated .gif).
|
||||
* `default` - type: PATH.
|
||||
|
@ -716,7 +718,7 @@ Can be created as an extra.
|
|||
* `rotation` - type: FLOAT.
|
||||
- angle in degrees that the text should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise.
|
||||
* `rotationOrigin` - type: NORMALIZED_PAIR.
|
||||
- Point around which the text will be rotated. Defaults to `0.5 0.5`.
|
||||
- Point around which the text will be rotated. Default is `0.5 0.5`.
|
||||
* `delay` - type: FLOAT. Default is false.
|
||||
- Delay in seconds before video will start playing.
|
||||
* `default` - type: PATH.
|
||||
|
@ -745,7 +747,7 @@ Can be created as an extra.
|
|||
* `rotation` - type: FLOAT.
|
||||
- angle in degrees that the text should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise.
|
||||
* `rotationOrigin` - type: NORMALIZED_PAIR.
|
||||
- Point around which the text will be rotated. Defaults to `0.5 0.5`.
|
||||
- Point around which the text will be rotated. Default is `0.5 0.5`.
|
||||
* `text` - type: STRING.
|
||||
* `color` - type: COLOR.
|
||||
* `backgroundColor` - type: COLOR;
|
||||
|
@ -817,7 +819,7 @@ ES-DE borrows the concept of "nine patches" from Android (or "9-Slices"). Curren
|
|||
* `rotation` - type: FLOAT.
|
||||
- angle in degrees that the rating should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise.
|
||||
* `rotationOrigin` - type: NORMALIZED_PAIR.
|
||||
- Point around which the rating will be rotated. Defaults to `0.5 0.5`.
|
||||
- Point around which the rating will be rotated. Default is `0.5 0.5`.
|
||||
* `filledPath` - type: PATH.
|
||||
- Path to the "filled star" image. Image must be square (width equals height).
|
||||
* `unfilledPath` - type: PATH.
|
||||
|
@ -841,7 +843,7 @@ ES-DE borrows the concept of "nine patches" from Android (or "9-Slices"). Curren
|
|||
* `rotation` - type: FLOAT.
|
||||
- angle in degrees that the text should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise.
|
||||
* `rotationOrigin` - type: NORMALIZED_PAIR.
|
||||
- Point around which the text will be rotated. Defaults to `0.5 0.5`.
|
||||
- Point around which the text will be rotated. Default is `0.5 0.5`.
|
||||
* `color` - type: COLOR.
|
||||
* `backgroundColor` - type: COLOR;
|
||||
* `fontPath` - type: PATH.
|
||||
|
@ -922,39 +924,35 @@ ES-DE borrows the concept of "nine patches" from Android (or "9-Slices"). Curren
|
|||
* `pos` - type: NORMALIZED_PAIR.
|
||||
* `size` - type: NORMALIZED_PAIR.
|
||||
- Possible combinations:
|
||||
- `w h` - Dimensions of the badges container. The badges will be scaled to fit within these dimensions.
|
||||
- `w h` - Dimensions of the badges container. The badges will be scaled to fit within these dimensions. Minimum value per axis is `0.03`, maximum value is `1.0`. Default is `0.15 0.20`.
|
||||
* `origin` - type: NORMALIZED_PAIR.
|
||||
- Where on the component `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the component exactly in the middle of the screen. If the "POSITION" and "SIZE" attributes are themeable, "ORIGIN" is implied.
|
||||
* `direction` - type: STRING.
|
||||
- Valid values are "row" or "column". Controls the primary layout direction (line axis) for the badges. Lines will fill up in the specified direction.
|
||||
* `align` - type: STRING.
|
||||
- Valid values are "start", "center", "end", or "stretch". Controls orthogonal alignment to the line axis. "stretch" will stretch the badges to fill-up the line width.
|
||||
* `itemsPerLine` - type: FLOAT.
|
||||
- Number of badges that fit on a line. When more badges are available a new line will be started.
|
||||
* `lines` - type: FLOAT.
|
||||
- The number of lines available.
|
||||
- Where on the component `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the component exactly in the middle of the screen. If the "POSITION" and "SIZE" attributes are themeable, "ORIGIN" is implied. Default is `0 0`.
|
||||
* `rotation` - type: FLOAT.
|
||||
- angle in degrees that the image should be rotated. Positive values will rotate clockwise, negative values will rotate counterclockwise. Default is `0`.
|
||||
* `rotationOrigin` - type: NORMALIZED_PAIR.
|
||||
- Point around which the image will be rotated. Default is `0.5 0.5`.
|
||||
* `itemsPerRow` - type: FLOAT.
|
||||
- Number of badges that fit on a row. When more badges are available a new row will be started. Default is `4`.
|
||||
* `rows` - type: FLOAT.
|
||||
- The number of rows available. Default is `2`.
|
||||
* `itemPlacement` - type: STRING.
|
||||
- Valid values are "top", "center", "bottom", or "stretch". Controls vertical alignment of each badge if images of different heights are used. "Stretch" will stretch the badge to the full height. Default is `center`.
|
||||
* `itemMargin` - type: NORMALIZED_PAIR.
|
||||
- The margins between badges. Possible combinations:
|
||||
- `x y` - horizontal and vertical margins.
|
||||
- `x y` - horizontal and vertical margins. Minimum value per axis is `0`, maximum value is `0.2`. Default is `0.01`.
|
||||
* `slots` - type: STRING.
|
||||
- The badge types that should be displayed. Should be specified as a dist of strings separated by spaces. The order will be followed when placing badges on the screen.
|
||||
- Available badges are:
|
||||
- "favorite": Will be shown when the game is marked as favorite.
|
||||
- "completed": Will be shown when the game is marked as completed.
|
||||
- "kidgame": Will be shown when the game is marked as a kids game.
|
||||
- "broken": Will be shown when the game is marked as broken.
|
||||
- "altemulator": Will be shown when an alternative emulator is setup for the game.
|
||||
- The badge types that should be displayed. Should be specified as a list of strings separated by spaces. The order will be followed when placing badges on the screen. Available badges are:
|
||||
- `favorite`: Will be shown when the game is marked as favorite.
|
||||
- `completed`: Will be shown when the game is marked as completed.
|
||||
- `kidgame`: Will be shown when the game is marked as a kids game.
|
||||
- `broken`: Will be shown when the game is marked as broken.
|
||||
- `altemulator`: Will be shown when an alternative emulator is setup for the game.
|
||||
* `customBadgeIcon` - type: PATH.
|
||||
- A badge icon override. Specify the badge type in the attribute `badge`. The available badges are:
|
||||
`favorite`,
|
||||
`completed`,
|
||||
`kidgame`,
|
||||
`broken`,
|
||||
`altemulator`
|
||||
- A badge icon override. Specify the badge type in the attribute `badge`. The available badges are the ones listed above.
|
||||
* `visible` - type: BOOLEAN.
|
||||
- If true, component will be rendered, otherwise rendering will be skipped. Can be used to hide elements from a particular view.
|
||||
* `zIndex` - type: FLOAT.
|
||||
- z-index value for component. Components will be rendered in order of z-index value from low to high.
|
||||
- z-index value for component. Components will be rendered in order of z-index value from low to high. Default is `50`.
|
||||
|
||||
#### carousel
|
||||
|
||||
|
@ -978,7 +976,7 @@ ES-DE borrows the concept of "nine patches" from Android (or "9-Slices"). Curren
|
|||
- Default is 7.5
|
||||
- This property only applies when `type` is "horizontal_wheel" or "vertical_wheel".
|
||||
* `logoRotationOrigin` - type: NORMALIZED_PAIR.
|
||||
- Point around which the logos will be rotated. Defaults to `-5 0.5`.
|
||||
- Point around which the logos will be rotated. Default is `-5 0.5`.
|
||||
- This property only applies when `type` is "horizontal_wheel" or "vertical_wheel".
|
||||
* `logoAlignment` - type: STRING.
|
||||
- Sets the alignment of the logos relative to the carousel.
|
||||
|
|
|
@ -213,6 +213,8 @@ In addition to the styles just described, there is a **Grid** view style as well
|
|||
|
||||
If the theme supports it, there's a gamelist information field displayed in the gamelist view, showing the number of games for the system (total and favorites) as well as a folder icon if a folder has been entered. When applying any filters to the gamelist, the game counter is replaced with the amount of games filtered, as in 'filtered / total games', e.g. '19 / 77'. If there are game entries in the filter result that are marked not to be counted as games, the number of such files will be indicated as 'filtered + filtered non-games / total games', for example '23 + 4 / 77' indicating 23 normal games, 4 non-games out of a total of 77. Due to this approach it's theoretically possible that the combined filtered game amount exceeds the number of counted games in the collection, for instance '69 + 11 / 77'. This is not considered a bug and is so by design. This gamelist information field functionality is specific to EmulationStation Desktop Edition so older themes will not support this.
|
||||
|
||||
Another feature which requires theme support is **Badges**, which is a set of icons displaying the status for various metadata fields. The currently supported badge types are _favorite, completed, kidgame, broken_ and _alternative emulator_. If any of the first four metadata fields have been set for a game, their corresponding badges will be displayed, and if an alternative emulator has been selected for the specific game, that badge will be shown. Setting an alternative emulator system-wide will not display this badge as it's only intended to indicate game-specific overrides.
|
||||
|
||||
![alt text](images/es-de_gamelist_view.png "ES-DE Gamelist View")
|
||||
_The **Gamelist view** is where you browse the games for a specific system._
|
||||
|
||||
|
@ -970,6 +972,10 @@ With this option enabled, there will be an overlay displayed when scrolling the
|
|||
|
||||
This enables a virtual (on-screen) keyboard that can be used at various places throughout the application to input text and numbers using a controller. The Shift and Alt keys can be toggled individually or combined together to access many special characters. The general use of the virtual keyboard should hopefully be self-explanatory.
|
||||
|
||||
**Enable menu scroll indicators**
|
||||
|
||||
With this option enabled, "up and down" scroll indicators will be displayed in the upper right corner of menus (including the metadata editor) if there are more entries available than can be shown on the screen at the same time. These indicators will change dynamically as the list is scrolled. If the setting is disabled, a simplified static indicator will be displayed instead.
|
||||
|
||||
**Enable toggle favorites button**
|
||||
|
||||
This setting enables the _Y_ button for quickly toggling a game as favorite. Although this may be convenient at times, it's also quite easy to accidentally remove a favorite tagging of a game when using the application more casually. As such it could sometimes make sense to disable this functionality. It's of course still possible to mark a game as favorite using the metadata editor when this setting is disabled. The option does not affect the use of the _Y_ button to add or remove games when editing custom collections.
|
||||
|
@ -1440,9 +1446,9 @@ A flag to indicate whether this is a favorite game. This flag can also be set di
|
|||
|
||||
A flag to indicate whether you have completed the game.
|
||||
|
||||
**Kidgame** _(files only)_
|
||||
**Kidgame**
|
||||
|
||||
A flag to mark whether the game is suitable for children. This will be applied as a filter when starting ES-DE in _Kid_ mode.
|
||||
A flag to mark whether the game is suitable for children. This will be applied as a filter when starting ES-DE in _Kid_ mode. Although it's possible to also set this flag for folders, this will **not** affect the actual files inside those folders. It will instead only be used to display the Kidgame badge for the folders themselves.
|
||||
|
||||
**Hidden**
|
||||
|
||||
|
@ -1462,7 +1468,7 @@ Whether to exclude the file from the multi-scraper. This is quite useful in orde
|
|||
|
||||
**Hide metadata fields**
|
||||
|
||||
This option will hide most metadata fields in the gamelist view. The intention is to be able to hide the fields for situations such as general folders (Multi-disc, Cartridges etc.) and for setup programs and similar (e.g. SETUP.EXE or INSTALL.BAT for DOS games). It could also be used on the game files for multi-disc games where perhaps only the .m3u playlist should have any metadata values. The only fields shown with this option enabled are the game name and description. Using the description it's possible to write some comments regarding the file or folder, should you want to. It's also possible to display game images and videos with this setting enabled.
|
||||
This option will hide most metadata fields as well as any badges. The intention is to be able to hide the fields for situations such as general folders (Multi-disc, Cartridges etc.) and for setup programs and similar (e.g. SETUP.EXE or INSTALL.BAT for DOS games). It could also be used on the game files for multi-disc games where perhaps only the .m3u playlist should have any metadata values. The only fields shown with this option enabled are the game name and description. Using the description it's possible to write some comments regarding the file or folder, should you want to. It's also possible to display game images and videos with this setting enabled.
|
||||
|
||||
**Times played** _(files only)_
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ MetaDataDecl folderDecls[] = {
|
|||
{"players", MD_STRING, "unknown", false, "players", "enter number of players", true},
|
||||
{"favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on", false},
|
||||
{"completed", MD_BOOL, "false", false, "completed", "enter completed off/on", false},
|
||||
{"kidgame", MD_BOOL, "false", false, "kidgame (only affects badges)", "enter kidgame off/on", false},
|
||||
{"hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on", false},
|
||||
{"broken", MD_BOOL, "false", false, "broken/not working", "enter broken off/on", false},
|
||||
{"nomultiscrape", MD_BOOL, "false", false, "exclude from multi-scraper", "enter no multi-scrape off/on", false},
|
||||
|
|
|
@ -248,6 +248,11 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window,
|
|||
"", ViewController::CROSSEDCIRCLE_CHAR + " CLEAR ENTRY"));
|
||||
|
||||
for (auto entry : launchCommands) {
|
||||
if (mInvalidEmulatorEntry && singleEntry &&
|
||||
entry.second !=
|
||||
ViewController::EXCLAMATION_CHAR + " " + originalValue)
|
||||
continue;
|
||||
|
||||
std::string selectedLabel = ed->getValue();
|
||||
std::string label;
|
||||
ComponentListRow row;
|
||||
|
|
|
@ -52,7 +52,8 @@ GuiScreensaverOptions::GuiScreensaverOptions(Window* window, const std::string&
|
|||
// If before it wasn't risky but now there's a risk of problems, show warning.
|
||||
mWindow->pushGui(new GuiMsgBox(
|
||||
mWindow, getHelpStyle(),
|
||||
"THE 'VIDEO' SCREENSAVER SHOWS\nVIDEOS FROM YOUR GAMELISTS\n\n"
|
||||
"THE 'VIDEO' SCREENSAVER SHOWS\n"
|
||||
"VIDEOS FROM YOUR GAMELISTS\n\n"
|
||||
"IF YOU DO NOT HAVE ANY VIDEOS, THE\n"
|
||||
"SCREENSAVER WILL DEFAULT TO 'DIM'",
|
||||
"OK", [] { return; }, "", nullptr, "", nullptr));
|
||||
|
|
|
@ -29,7 +29,6 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root)
|
|||
, mLblPlayers(window)
|
||||
, mLblLastPlayed(window)
|
||||
, mLblPlayCount(window)
|
||||
, mBadges(window)
|
||||
, mRating(window)
|
||||
, mReleaseDate(window)
|
||||
, mDeveloper(window)
|
||||
|
@ -39,6 +38,7 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root)
|
|||
, mLastPlayed(window)
|
||||
, mPlayCount(window)
|
||||
, mName(window)
|
||||
, mBadges(window)
|
||||
, mDescContainer(window)
|
||||
, mDescription(window)
|
||||
, mGamelistInfo(window)
|
||||
|
@ -76,33 +76,39 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root)
|
|||
addChild(&mImage);
|
||||
|
||||
// Metadata labels + values.
|
||||
mLblRating.setText("Rating: ");
|
||||
mLblRating.setText("Rating: ", false);
|
||||
addChild(&mLblRating);
|
||||
addChild(&mRating);
|
||||
mLblReleaseDate.setText("Released: ");
|
||||
mLblReleaseDate.setText("Released: ", false);
|
||||
addChild(&mLblReleaseDate);
|
||||
addChild(&mReleaseDate);
|
||||
mLblDeveloper.setText("Developer: ");
|
||||
mLblDeveloper.setText("Developer: ", false);
|
||||
addChild(&mLblDeveloper);
|
||||
addChild(&mDeveloper);
|
||||
mLblPublisher.setText("Publisher: ");
|
||||
mLblPublisher.setText("Publisher: ", false);
|
||||
addChild(&mLblPublisher);
|
||||
addChild(&mPublisher);
|
||||
mLblGenre.setText("Genre: ");
|
||||
mLblGenre.setText("Genre: ", false);
|
||||
addChild(&mLblGenre);
|
||||
addChild(&mGenre);
|
||||
mLblPlayers.setText("Players: ");
|
||||
mLblPlayers.setText("Players: ", false);
|
||||
addChild(&mLblPlayers);
|
||||
addChild(&mPlayers);
|
||||
addChild(&mBadges);
|
||||
mLblLastPlayed.setText("Last played: ");
|
||||
mLblLastPlayed.setText("Last played: ", false);
|
||||
addChild(&mLblLastPlayed);
|
||||
mLastPlayed.setDisplayRelative(true);
|
||||
addChild(&mLastPlayed);
|
||||
mLblPlayCount.setText("Times played: ");
|
||||
mLblPlayCount.setText("Times played: ", false);
|
||||
addChild(&mLblPlayCount);
|
||||
addChild(&mPlayCount);
|
||||
|
||||
// Badges.
|
||||
addChild(&mBadges);
|
||||
mBadges.setOrigin(0.0f, 0.0f);
|
||||
mBadges.setPosition(mSize.x * 0.8f, mSize.y * 0.7f);
|
||||
mBadges.setSize(mSize.x * 0.15f, mSize.y * 0.2f);
|
||||
mBadges.setDefaultZIndex(50.0f);
|
||||
|
||||
mName.setPosition(mSize.x, mSize.y);
|
||||
mName.setDefaultZIndex(40.0f);
|
||||
mName.setColor(0xAAAAAAFF);
|
||||
|
@ -143,6 +149,7 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr<ThemeData>& them
|
|||
mImage.applyTheme(theme, getName(), "md_image",
|
||||
POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION | VISIBLE);
|
||||
mName.applyTheme(theme, getName(), "md_name", ALL);
|
||||
mBadges.applyTheme(theme, getName(), "md_badges", ALL);
|
||||
|
||||
initMDLabels();
|
||||
std::vector<TextComponent*> labels = getMDLabels();
|
||||
|
@ -156,10 +163,10 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr<ThemeData>& them
|
|||
|
||||
initMDValues();
|
||||
std::vector<GuiComponent*> values = getMDValues();
|
||||
assert(values.size() == 9);
|
||||
assert(values.size() == 8);
|
||||
std::vector<std::string> valElements = {"md_rating", "md_releasedate", "md_developer",
|
||||
"md_publisher", "md_genre", "md_players",
|
||||
"md_badges", "md_lastplayed", "md_playcount"};
|
||||
"md_lastplayed", "md_playcount"};
|
||||
|
||||
for (unsigned int i = 0; i < values.size(); i++)
|
||||
values[i]->applyTheme(theme, getName(), valElements[i], ALL ^ ThemeFlags::TEXT);
|
||||
|
@ -226,8 +233,6 @@ void DetailedGameListView::initMDValues()
|
|||
mLastPlayed.setFont(defaultFont);
|
||||
mPlayCount.setFont(defaultFont);
|
||||
|
||||
mBadges.setSize(defaultFont->getHeight() * 5.0f, static_cast<float>(defaultFont->getHeight()));
|
||||
|
||||
float bottom = 0.0f;
|
||||
|
||||
const float colSize = (mSize.x * 0.48f) / 2.0f;
|
||||
|
@ -297,11 +302,11 @@ void DetailedGameListView::updateInfoPanel()
|
|||
mGenre.setVisible(false);
|
||||
mLblPlayers.setVisible(false);
|
||||
mPlayers.setVisible(false);
|
||||
mBadges.setVisible(false);
|
||||
mLblLastPlayed.setVisible(false);
|
||||
mLastPlayed.setVisible(false);
|
||||
mLblPlayCount.setVisible(false);
|
||||
mPlayCount.setVisible(false);
|
||||
mBadges.setVisible(false);
|
||||
}
|
||||
else {
|
||||
mLblRating.setVisible(true);
|
||||
|
@ -316,11 +321,11 @@ void DetailedGameListView::updateInfoPanel()
|
|||
mGenre.setVisible(true);
|
||||
mLblPlayers.setVisible(true);
|
||||
mPlayers.setVisible(true);
|
||||
mBadges.setVisible(true);
|
||||
mLblLastPlayed.setVisible(true);
|
||||
mLastPlayed.setVisible(true);
|
||||
mLblPlayCount.setVisible(true);
|
||||
mPlayCount.setVisible(true);
|
||||
mBadges.setVisible(true);
|
||||
}
|
||||
|
||||
bool fadingOut = false;
|
||||
|
@ -443,6 +448,7 @@ void DetailedGameListView::updateInfoPanel()
|
|||
comps.push_back(&mImage);
|
||||
comps.push_back(&mDescription);
|
||||
comps.push_back(&mName);
|
||||
comps.push_back(&mBadges);
|
||||
std::vector<TextComponent*> labels = getMDLabels();
|
||||
comps.insert(comps.cend(), labels.cbegin(), labels.cend());
|
||||
|
||||
|
@ -488,7 +494,6 @@ std::vector<GuiComponent*> DetailedGameListView::getMDValues()
|
|||
ret.push_back(&mPublisher);
|
||||
ret.push_back(&mGenre);
|
||||
ret.push_back(&mPlayers);
|
||||
ret.push_back(&mBadges);
|
||||
ret.push_back(&mLastPlayed);
|
||||
ret.push_back(&mPlayCount);
|
||||
return ret;
|
||||
|
|
|
@ -47,7 +47,6 @@ private:
|
|||
TextComponent mLblLastPlayed;
|
||||
TextComponent mLblPlayCount;
|
||||
|
||||
BadgesComponent mBadges;
|
||||
RatingComponent mRating;
|
||||
DateTimeComponent mReleaseDate;
|
||||
TextComponent mDeveloper;
|
||||
|
@ -57,6 +56,7 @@ private:
|
|||
DateTimeComponent mLastPlayed;
|
||||
TextComponent mPlayCount;
|
||||
TextComponent mName;
|
||||
BadgesComponent mBadges;
|
||||
|
||||
std::vector<TextComponent*> getMDLabels();
|
||||
std::vector<GuiComponent*> getMDValues();
|
||||
|
|
|
@ -57,29 +57,29 @@ GridGameListView::GridGameListView(Window* window, FileData* root)
|
|||
|
||||
// Metadata labels + values.
|
||||
addChild(&mBadges);
|
||||
mLblRating.setText("Rating: ");
|
||||
mLblRating.setText("Rating: ", false);
|
||||
addChild(&mLblRating);
|
||||
addChild(&mRating);
|
||||
mLblReleaseDate.setText("Released: ");
|
||||
mLblReleaseDate.setText("Released: ", false);
|
||||
addChild(&mLblReleaseDate);
|
||||
addChild(&mReleaseDate);
|
||||
mLblDeveloper.setText("Developer: ");
|
||||
mLblDeveloper.setText("Developer: ", false);
|
||||
addChild(&mLblDeveloper);
|
||||
addChild(&mDeveloper);
|
||||
mLblPublisher.setText("Publisher: ");
|
||||
mLblPublisher.setText("Publisher: ", false);
|
||||
addChild(&mLblPublisher);
|
||||
addChild(&mPublisher);
|
||||
mLblGenre.setText("Genre: ");
|
||||
mLblGenre.setText("Genre: ", false);
|
||||
addChild(&mLblGenre);
|
||||
addChild(&mGenre);
|
||||
mLblPlayers.setText("Players: ");
|
||||
mLblPlayers.setText("Players: ", false);
|
||||
addChild(&mLblPlayers);
|
||||
addChild(&mPlayers);
|
||||
mLblLastPlayed.setText("Last played: ");
|
||||
mLblLastPlayed.setText("Last played: ", false);
|
||||
addChild(&mLblLastPlayed);
|
||||
mLastPlayed.setDisplayRelative(true);
|
||||
addChild(&mLastPlayed);
|
||||
mLblPlayCount.setText("Times played: ");
|
||||
mLblPlayCount.setText("Times played: ", false);
|
||||
addChild(&mLblPlayCount);
|
||||
addChild(&mPlayCount);
|
||||
|
||||
|
|
|
@ -42,10 +42,10 @@ VideoGameListView::VideoGameListView(Window* window, FileData* root)
|
|||
, mPublisher(window)
|
||||
, mGenre(window)
|
||||
, mPlayers(window)
|
||||
, mBadges(window)
|
||||
, mLastPlayed(window)
|
||||
, mPlayCount(window)
|
||||
, mName(window)
|
||||
, mBadges(window)
|
||||
, mDescContainer(window)
|
||||
, mDescription(window)
|
||||
, mGamelistInfo(window)
|
||||
|
@ -93,33 +93,39 @@ VideoGameListView::VideoGameListView(Window* window, FileData* root)
|
|||
addChild(mVideo);
|
||||
|
||||
// Metadata labels + values.
|
||||
mLblRating.setText("Rating: ");
|
||||
mLblRating.setText("Rating: ", false);
|
||||
addChild(&mLblRating);
|
||||
addChild(&mRating);
|
||||
mLblReleaseDate.setText("Released: ");
|
||||
mLblReleaseDate.setText("Released: ", false);
|
||||
addChild(&mLblReleaseDate);
|
||||
addChild(&mReleaseDate);
|
||||
mLblDeveloper.setText("Developer: ");
|
||||
mLblDeveloper.setText("Developer: ", false);
|
||||
addChild(&mLblDeveloper);
|
||||
addChild(&mDeveloper);
|
||||
mLblPublisher.setText("Publisher: ");
|
||||
mLblPublisher.setText("Publisher: ", false);
|
||||
addChild(&mLblPublisher);
|
||||
addChild(&mPublisher);
|
||||
mLblGenre.setText("Genre: ");
|
||||
mLblGenre.setText("Genre: ", false);
|
||||
addChild(&mLblGenre);
|
||||
addChild(&mGenre);
|
||||
mLblPlayers.setText("Players: ");
|
||||
mLblPlayers.setText("Players: ", false);
|
||||
addChild(&mLblPlayers);
|
||||
addChild(&mPlayers);
|
||||
addChild(&mBadges);
|
||||
mLblLastPlayed.setText("Last played: ");
|
||||
mLblLastPlayed.setText("Last played: ", false);
|
||||
addChild(&mLblLastPlayed);
|
||||
mLastPlayed.setDisplayRelative(true);
|
||||
addChild(&mLastPlayed);
|
||||
mLblPlayCount.setText("Times played: ");
|
||||
mLblPlayCount.setText("Times played: ", false);
|
||||
addChild(&mLblPlayCount);
|
||||
addChild(&mPlayCount);
|
||||
|
||||
// Badges.
|
||||
addChild(&mBadges);
|
||||
mBadges.setOrigin(0.0f, 0.0f);
|
||||
mBadges.setPosition(mSize.x * 0.8f, mSize.y * 0.7f);
|
||||
mBadges.setSize(mSize.x * 0.15f, mSize.y * 0.2f);
|
||||
mBadges.setDefaultZIndex(50.0f);
|
||||
|
||||
mName.setPosition(mSize.x, mSize.y);
|
||||
mName.setDefaultZIndex(40.0f);
|
||||
mName.setColor(0xAAAAAAFF);
|
||||
|
@ -165,6 +171,7 @@ void VideoGameListView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
|
|||
POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION |
|
||||
VISIBLE);
|
||||
mName.applyTheme(theme, getName(), "md_name", ALL);
|
||||
mBadges.applyTheme(theme, getName(), "md_badges", ALL);
|
||||
|
||||
initMDLabels();
|
||||
std::vector<TextComponent*> labels = getMDLabels();
|
||||
|
@ -178,10 +185,10 @@ void VideoGameListView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
|
|||
|
||||
initMDValues();
|
||||
std::vector<GuiComponent*> values = getMDValues();
|
||||
assert(values.size() == 9);
|
||||
assert(values.size() == 8);
|
||||
std::vector<std::string> valElements = {"md_rating", "md_releasedate", "md_developer",
|
||||
"md_publisher", "md_genre", "md_players",
|
||||
"md_badges", "md_lastplayed", "md_playcount"};
|
||||
"md_lastplayed", "md_playcount"};
|
||||
|
||||
for (unsigned int i = 0; i < values.size(); i++)
|
||||
values[i]->applyTheme(theme, getName(), valElements[i], ALL ^ ThemeFlags::TEXT);
|
||||
|
@ -245,9 +252,6 @@ void VideoGameListView::initMDValues()
|
|||
mPublisher.setFont(defaultFont);
|
||||
mGenre.setFont(defaultFont);
|
||||
mPlayers.setFont(defaultFont);
|
||||
|
||||
mBadges.setSize(defaultFont->getHeight() * 5.0f, static_cast<float>(defaultFont->getHeight()));
|
||||
|
||||
mLastPlayed.setFont(defaultFont);
|
||||
mPlayCount.setFont(defaultFont);
|
||||
|
||||
|
@ -320,11 +324,11 @@ void VideoGameListView::updateInfoPanel()
|
|||
mGenre.setVisible(false);
|
||||
mLblPlayers.setVisible(false);
|
||||
mPlayers.setVisible(false);
|
||||
mBadges.setVisible(false);
|
||||
mLblLastPlayed.setVisible(false);
|
||||
mLastPlayed.setVisible(false);
|
||||
mLblPlayCount.setVisible(false);
|
||||
mPlayCount.setVisible(false);
|
||||
mBadges.setVisible(false);
|
||||
}
|
||||
else {
|
||||
mLblRating.setVisible(true);
|
||||
|
@ -339,11 +343,11 @@ void VideoGameListView::updateInfoPanel()
|
|||
mGenre.setVisible(true);
|
||||
mLblPlayers.setVisible(true);
|
||||
mPlayers.setVisible(true);
|
||||
mBadges.setVisible(true);
|
||||
mLblLastPlayed.setVisible(true);
|
||||
mLastPlayed.setVisible(true);
|
||||
mLblPlayCount.setVisible(true);
|
||||
mPlayCount.setVisible(true);
|
||||
mBadges.setVisible(true);
|
||||
}
|
||||
|
||||
bool fadingOut = false;
|
||||
|
@ -484,6 +488,7 @@ void VideoGameListView::updateInfoPanel()
|
|||
comps.push_back(mVideo);
|
||||
comps.push_back(&mDescription);
|
||||
comps.push_back(&mName);
|
||||
comps.push_back(&mBadges);
|
||||
std::vector<TextComponent*> labels = getMDLabels();
|
||||
comps.insert(comps.cend(), labels.cbegin(), labels.cend());
|
||||
|
||||
|
@ -526,7 +531,6 @@ std::vector<GuiComponent*> VideoGameListView::getMDValues()
|
|||
ret.push_back(&mPublisher);
|
||||
ret.push_back(&mGenre);
|
||||
ret.push_back(&mPlayers);
|
||||
ret.push_back(&mBadges);
|
||||
ret.push_back(&mLastPlayed);
|
||||
ret.push_back(&mPlayCount);
|
||||
return ret;
|
||||
|
|
|
@ -57,10 +57,10 @@ private:
|
|||
TextComponent mPublisher;
|
||||
TextComponent mGenre;
|
||||
TextComponent mPlayers;
|
||||
BadgesComponent mBadges;
|
||||
DateTimeComponent mLastPlayed;
|
||||
TextComponent mPlayCount;
|
||||
TextComponent mName;
|
||||
BadgesComponent mBadges;
|
||||
|
||||
std::vector<TextComponent*> getMDLabels();
|
||||
std::vector<GuiComponent*> getMDValues();
|
||||
|
|
|
@ -150,10 +150,12 @@ std::map<std::string, std::map<std::string, ThemeData::ElementPropertyType>> The
|
|||
{{"pos", NORMALIZED_PAIR},
|
||||
{"size", NORMALIZED_PAIR},
|
||||
{"origin", NORMALIZED_PAIR},
|
||||
{"direction", STRING},
|
||||
{"align", STRING},
|
||||
{"itemsPerLine", FLOAT},
|
||||
{"lines", FLOAT},
|
||||
{"rotation", FLOAT},
|
||||
{"rotationOrigin", NORMALIZED_PAIR},
|
||||
{"alignment", STRING},
|
||||
{"itemsPerRow", FLOAT},
|
||||
{"rows", FLOAT},
|
||||
{"itemPlacement", STRING},
|
||||
{"itemMargin", NORMALIZED_PAIR},
|
||||
{"slots", STRING},
|
||||
{"customBadgeIcon", PATH},
|
||||
|
|
|
@ -54,7 +54,6 @@ namespace ThemeFlags
|
|||
Z_INDEX = 8192,
|
||||
ROTATION = 16384,
|
||||
VISIBLE = 32768,
|
||||
DIRECTION = 65536,
|
||||
ALL = 0xFFFFFFFF
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,30 +4,31 @@
|
|||
// BadgesComponent.cpp
|
||||
//
|
||||
// Game badges icons.
|
||||
// Used by gamelist views.
|
||||
// Used by the gamelist views.
|
||||
//
|
||||
|
||||
#define SLOT_FAVORITE "favorite"
|
||||
#define SLOT_COMPLETED "completed"
|
||||
#define SLOT_KIDGAME "kidgame"
|
||||
#define SLOT_BROKEN "broken"
|
||||
#define SLOT_ALTERNATIVE_EMULATOR "altemulator"
|
||||
#define SLOT_ALTEMULATOR "altemulator"
|
||||
|
||||
#include "components/BadgesComponent.h"
|
||||
|
||||
#include "Log.h"
|
||||
#include "ThemeData.h"
|
||||
#include "utils/StringUtil.h"
|
||||
|
||||
BadgesComponent::BadgesComponent(Window* window)
|
||||
: FlexboxComponent{window, mBadgeImages}
|
||||
, mBadgeTypes{
|
||||
{SLOT_FAVORITE, SLOT_COMPLETED, SLOT_KIDGAME, SLOT_BROKEN, SLOT_ALTERNATIVE_EMULATOR}}
|
||||
: GuiComponent{window}
|
||||
, mFlexboxComponent{window, mBadgeImages}
|
||||
, mBadgeTypes{{SLOT_FAVORITE, SLOT_COMPLETED, SLOT_KIDGAME, SLOT_BROKEN, SLOT_ALTEMULATOR}}
|
||||
{
|
||||
mBadgeIcons[SLOT_FAVORITE] = ":/graphics/badge_favorite.svg";
|
||||
mBadgeIcons[SLOT_COMPLETED] = ":/graphics/badge_completed.svg";
|
||||
mBadgeIcons[SLOT_KIDGAME] = ":/graphics/badge_kidgame.svg";
|
||||
mBadgeIcons[SLOT_BROKEN] = ":/graphics/badge_broken.svg";
|
||||
mBadgeIcons[SLOT_ALTERNATIVE_EMULATOR] = ":/graphics/badge_altemulator.svg";
|
||||
mBadgeIcons[SLOT_ALTEMULATOR] = ":/graphics/badge_altemulator.svg";
|
||||
}
|
||||
|
||||
void BadgesComponent::setBadges(const std::vector<std::string>& badges)
|
||||
|
@ -52,12 +53,27 @@ void BadgesComponent::setBadges(const std::vector<std::string>& badges)
|
|||
// Only recalculate the flexbox if any badges changed.
|
||||
for (auto& image : mBadgeImages) {
|
||||
if (prevVisibility[image.first] != image.second.isVisible()) {
|
||||
onSizeChanged();
|
||||
mFlexboxComponent.onSizeChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BadgesComponent::render(const glm::mat4& parentTrans)
|
||||
{
|
||||
if (!isVisible())
|
||||
return;
|
||||
|
||||
if (mOpacity == 255) {
|
||||
mFlexboxComponent.render(parentTrans);
|
||||
}
|
||||
else {
|
||||
mFlexboxComponent.setOpacity(mOpacity);
|
||||
mFlexboxComponent.render(parentTrans);
|
||||
mFlexboxComponent.setOpacity(255);
|
||||
}
|
||||
}
|
||||
|
||||
void BadgesComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
||||
const std::string& view,
|
||||
const std::string& element,
|
||||
|
@ -69,6 +85,70 @@ void BadgesComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
|||
if (!elem)
|
||||
return;
|
||||
|
||||
if (elem->has("alignment")) {
|
||||
const std::string alignment{elem->get<std::string>("alignment")};
|
||||
if (alignment != "left" && alignment != "right") {
|
||||
LOG(LogWarning) << "BadgesComponent: Invalid theme configuration, <alignment> set to \""
|
||||
<< alignment << "\"";
|
||||
}
|
||||
else {
|
||||
mFlexboxComponent.setAlignment(alignment);
|
||||
}
|
||||
}
|
||||
|
||||
if (elem->has("itemsPerRow")) {
|
||||
const float itemsPerRow{elem->get<float>("itemsPerRow")};
|
||||
if (itemsPerRow < 1.0f || itemsPerRow > 10.0f) {
|
||||
LOG(LogWarning)
|
||||
<< "BadgesComponent: Invalid theme configuration, <itemsPerRow> set to \""
|
||||
<< itemsPerRow << "\"";
|
||||
}
|
||||
else {
|
||||
mFlexboxComponent.setItemsPerLine(static_cast<unsigned int>(itemsPerRow));
|
||||
}
|
||||
}
|
||||
|
||||
if (elem->has("rows")) {
|
||||
const float rows{elem->get<float>("rows")};
|
||||
if (rows < 1.0f || rows > 10.0f) {
|
||||
LOG(LogWarning) << "BadgesComponent: Invalid theme configuration, <rows> set to \""
|
||||
<< rows << "\"";
|
||||
}
|
||||
else {
|
||||
mFlexboxComponent.setLines(static_cast<unsigned int>(rows));
|
||||
}
|
||||
}
|
||||
|
||||
if (elem->has("itemPlacement")) {
|
||||
std::string itemPlacement{elem->get<std::string>("itemPlacement")};
|
||||
if (itemPlacement != "top" && itemPlacement != "center" && itemPlacement != "bottom" &&
|
||||
itemPlacement != "stretch") {
|
||||
LOG(LogWarning)
|
||||
<< "BadgesComponent: Invalid theme configuration, <itemPlacement> set to \""
|
||||
<< itemPlacement << "\"";
|
||||
}
|
||||
else {
|
||||
if (itemPlacement == "top")
|
||||
itemPlacement = "start";
|
||||
else if (itemPlacement == "bottom")
|
||||
itemPlacement = "end";
|
||||
mFlexboxComponent.setItemPlacement(itemPlacement);
|
||||
}
|
||||
}
|
||||
|
||||
if (elem->has("itemMargin")) {
|
||||
const glm::vec2 itemMargin = elem->get<glm::vec2>("itemMargin");
|
||||
if (itemMargin.x < 0.0f || itemMargin.x > 0.2f || itemMargin.y < 0.0f ||
|
||||
itemMargin.y > 0.2f) {
|
||||
LOG(LogWarning)
|
||||
<< "BadgesComponent: Invalid theme configuration, <itemMargin> set to \""
|
||||
<< itemMargin.x << " " << itemMargin.y << "\"";
|
||||
}
|
||||
else {
|
||||
mFlexboxComponent.setItemMargin(itemMargin);
|
||||
}
|
||||
}
|
||||
|
||||
if (elem->has("slots")) {
|
||||
std::vector<std::string> slots = Utils::String::delimitedStringToVector(
|
||||
Utils::String::toLower(elem->get<std::string>("slots")), " ");
|
||||
|
@ -89,7 +169,15 @@ void BadgesComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
|||
}
|
||||
}
|
||||
|
||||
// Apply theme on the flexbox component parent.
|
||||
FlexboxComponent::applyTheme(theme, view, element, properties);
|
||||
GuiComponent::applyTheme(theme, view, element, properties);
|
||||
|
||||
mFlexboxComponent.setPosition(mPosition);
|
||||
mFlexboxComponent.setSize(mSize);
|
||||
mFlexboxComponent.setOrigin(mOrigin);
|
||||
mFlexboxComponent.setRotation(mRotation);
|
||||
mFlexboxComponent.setRotationOrigin(mRotationOrigin);
|
||||
mFlexboxComponent.setVisible(mVisible);
|
||||
mFlexboxComponent.setDefaultZIndex(mDefaultZIndex);
|
||||
mFlexboxComponent.setZIndex(mZIndex);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,15 +4,16 @@
|
|||
// BadgesComponent.h
|
||||
//
|
||||
// Game badges icons.
|
||||
// Used by gamelist views.
|
||||
// Used by the gamelist views.
|
||||
//
|
||||
|
||||
#ifndef ES_CORE_COMPONENTS_BADGES_COMPONENT_H
|
||||
#define ES_CORE_COMPONENTS_BADGES_COMPONENT_H
|
||||
|
||||
#include "FlexboxComponent.h"
|
||||
#include "GuiComponent.h"
|
||||
|
||||
class BadgesComponent : public FlexboxComponent
|
||||
class BadgesComponent : public GuiComponent
|
||||
{
|
||||
public:
|
||||
BadgesComponent(Window* window);
|
||||
|
@ -20,12 +21,17 @@ public:
|
|||
std::vector<std::string> getBadgeTypes() { return mBadgeTypes; }
|
||||
void setBadges(const std::vector<std::string>& badges);
|
||||
|
||||
void render(const glm::mat4& parentTrans) override;
|
||||
void onSizeChanged() override { mFlexboxComponent.onSizeChanged(); }
|
||||
|
||||
virtual void applyTheme(const std::shared_ptr<ThemeData>& theme,
|
||||
const std::string& view,
|
||||
const std::string& element,
|
||||
unsigned int properties) override;
|
||||
|
||||
private:
|
||||
FlexboxComponent mFlexboxComponent;
|
||||
|
||||
std::vector<std::string> mBadgeTypes;
|
||||
std::map<std::string, std::string> mBadgeIcons;
|
||||
std::vector<std::pair<std::string, ImageComponent>> mBadgeImages;
|
||||
|
|
|
@ -474,22 +474,42 @@ std::vector<HelpPrompt> ComponentGrid::getHelpPrompts()
|
|||
if (e)
|
||||
prompts = e->component->getHelpPrompts();
|
||||
|
||||
bool canScrollVert = mGridSize.y > 1;
|
||||
bool canScrollHoriz = mGridSize.x > 1;
|
||||
for (auto it = prompts.cbegin(); it != prompts.cend(); it++) {
|
||||
if (it->first == "up/down/left/right") {
|
||||
canScrollHoriz = false;
|
||||
canScrollVert = false;
|
||||
break;
|
||||
bool canScrollVert = false;
|
||||
|
||||
// If the currently selected cell does not fill the entire Y axis, then check if the cells
|
||||
// above or below are actually focusable as otherwise they should not affect the help prompts.
|
||||
if (mGridSize.y > 1 && e->dim.y < mGridSize.y) {
|
||||
if (e->pos.y - e->dim.y >= 0) {
|
||||
const GridEntry* cell = getCellAt(glm::ivec2{e->pos.x, e->pos.y - e->dim.y});
|
||||
if (cell != nullptr && cell->canFocus)
|
||||
canScrollVert = true;
|
||||
}
|
||||
else if (it->first == "up/down") {
|
||||
canScrollVert = false;
|
||||
}
|
||||
else if (it->first == "left/right") {
|
||||
canScrollHoriz = false;
|
||||
if (e->pos.y + e->dim.y < mGridSize.y) {
|
||||
const GridEntry* cell = getCellAt(glm::ivec2{e->pos.x, e->pos.y + e->dim.y});
|
||||
if (cell != nullptr && cell->canFocus)
|
||||
canScrollVert = true;
|
||||
}
|
||||
}
|
||||
|
||||
// There is currently no situation in the application where unfocusable cells are located
|
||||
// next to each other horizontally, so this code is good enough. If this changes in the
|
||||
// future, code similar to the the vertical cell handling above needs to be added.
|
||||
bool canScrollHoriz = (mGridSize.x > 1 && e->dim.x < mGridSize.x);
|
||||
|
||||
// Check existing capabilities as indicated by the help prompts, and if the prompts should
|
||||
// be combined into "up/down/left/right" then also remove the single-axis prompts.
|
||||
if (!prompts.empty() && prompts.back() == HelpPrompt("up/down", "choose")) {
|
||||
canScrollVert = true;
|
||||
if (canScrollHoriz && canScrollVert)
|
||||
prompts.pop_back();
|
||||
}
|
||||
else if (!prompts.empty() && prompts.back() == HelpPrompt("left/right", "choose")) {
|
||||
canScrollHoriz = true;
|
||||
if (canScrollHoriz && canScrollVert)
|
||||
prompts.pop_back();
|
||||
}
|
||||
|
||||
// Any duplicates will be removed in Window::setHelpPrompts()
|
||||
if (canScrollHoriz && canScrollVert)
|
||||
prompts.push_back(HelpPrompt("up/down/left/right", "choose"));
|
||||
else if (canScrollHoriz)
|
||||
|
|
|
@ -4,58 +4,105 @@
|
|||
// FlexboxComponent.cpp
|
||||
//
|
||||
// Flexbox layout component.
|
||||
// Used by gamelist views.
|
||||
//
|
||||
|
||||
#define DEFAULT_DIRECTION Direction::row
|
||||
#define DEFAULT_ALIGN Align::center
|
||||
#define DEFAULT_DIRECTION "row"
|
||||
#define DEFAULT_ALIGNMENT "left"
|
||||
#define DEFAULT_ITEMS_PER_LINE 4
|
||||
#define DEFAULT_LINES 1
|
||||
#define DEFAULT_MARGIN_X 10.0f
|
||||
#define DEFAULT_MARGIN_Y 10.0f
|
||||
#define DEFAULT_LINES 2
|
||||
#define DEFAULT_ITEM_PLACEMENT "center"
|
||||
#define DEFAULT_MARGIN_X std::roundf(0.01f * Renderer::getScreenWidth())
|
||||
#define DEFAULT_MARGIN_Y std::roundf(0.01f * Renderer::getScreenHeight())
|
||||
|
||||
#include "components/FlexboxComponent.h"
|
||||
|
||||
#include "Settings.h"
|
||||
#include "ThemeData.h"
|
||||
|
||||
FlexboxComponent::FlexboxComponent(Window* window,
|
||||
std::vector<std::pair<std::string, ImageComponent>>& images)
|
||||
: GuiComponent{window}
|
||||
, mDirection{DEFAULT_DIRECTION}
|
||||
, mAlign{DEFAULT_ALIGN}
|
||||
, mImages(images)
|
||||
, mDirection{DEFAULT_DIRECTION}
|
||||
, mAlignment{DEFAULT_ALIGNMENT}
|
||||
, mItemsPerLine{DEFAULT_ITEMS_PER_LINE}
|
||||
, mLines{DEFAULT_LINES}
|
||||
, mItemPlacement{DEFAULT_ITEM_PLACEMENT}
|
||||
, mItemMargin{glm::vec2{DEFAULT_MARGIN_X, DEFAULT_MARGIN_Y}}
|
||||
, mLayoutValid{false}
|
||||
{
|
||||
}
|
||||
|
||||
void FlexboxComponent::render(const glm::mat4& parentTrans)
|
||||
{
|
||||
if (!isVisible())
|
||||
return;
|
||||
|
||||
if (!mLayoutValid)
|
||||
computeLayout();
|
||||
|
||||
glm::mat4 trans{parentTrans * getTransform()};
|
||||
Renderer::setMatrix(trans);
|
||||
|
||||
if (Settings::getInstance()->getBool("DebugImage"))
|
||||
Renderer::drawRect(0.0f, 0.0f, mSize.x, mSize.y, 0xFF000033, 0xFF000033);
|
||||
|
||||
for (auto& image : mImages) {
|
||||
if (mOpacity == 255) {
|
||||
image.second.render(trans);
|
||||
}
|
||||
else {
|
||||
image.second.setOpacity(mOpacity);
|
||||
image.second.render(trans);
|
||||
image.second.setOpacity(255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FlexboxComponent::computeLayout()
|
||||
{
|
||||
// Start placing items in the top-left.
|
||||
float anchorX{0.0f};
|
||||
float anchorY{0.0f};
|
||||
float anchorOriginX{0.0f};
|
||||
float anchorOriginY{0.0f};
|
||||
|
||||
// Translation directions when placing items.
|
||||
glm::ivec2 directionLine{1, 0};
|
||||
glm::ivec2 directionRow{0, 1};
|
||||
glm::vec2 directionLine{1, 0};
|
||||
glm::vec2 directionRow{0, 1};
|
||||
|
||||
// Change direction.
|
||||
if (mDirection == Direction::column) {
|
||||
if (mDirection == "column") {
|
||||
directionLine = {0, 1};
|
||||
directionRow = {1, 0};
|
||||
}
|
||||
|
||||
// If we're not clamping itemMargin to a reasonable value, all kinds of weird rendering
|
||||
// issues could occur.
|
||||
mItemMargin.x = glm::clamp(mItemMargin.x, 0.0f, mSize.x / 2.0f);
|
||||
mItemMargin.y = glm::clamp(mItemMargin.y, 0.0f, mSize.y / 2.0f);
|
||||
|
||||
// Also keep the size within reason.
|
||||
mSize.x = glm::clamp(mSize.x, static_cast<float>(Renderer::getScreenWidth()) * 0.03f,
|
||||
static_cast<float>(Renderer::getScreenWidth()));
|
||||
mSize.y = glm::clamp(mSize.y, static_cast<float>(Renderer::getScreenHeight()) * 0.03f,
|
||||
static_cast<float>(Renderer::getScreenHeight()));
|
||||
|
||||
// Compute maximum image dimensions.
|
||||
glm::vec2 grid;
|
||||
if (mDirection == Direction::row)
|
||||
if (mDirection == "row")
|
||||
grid = {mItemsPerLine, mLines};
|
||||
else
|
||||
grid = {mLines, mItemsPerLine};
|
||||
|
||||
glm::vec2 maxItemSize{(mSize + mItemMargin - grid * mItemMargin) / grid};
|
||||
maxItemSize.x = floorf(maxItemSize.x);
|
||||
maxItemSize.y = floorf(maxItemSize.y);
|
||||
|
||||
if (grid.x * grid.y < static_cast<float>(mImages.size())) {
|
||||
LOG(LogWarning) << "FlexboxComponent: Invalid theme configuration, the number of badges "
|
||||
"exceeds the product of <lines> times <itemsPerLine>";
|
||||
}
|
||||
|
||||
glm::vec2 sizeChange{0.0f, 0.0f};
|
||||
|
||||
// Set final image dimensions.
|
||||
for (auto& image : mImages) {
|
||||
|
@ -73,12 +120,23 @@ void FlexboxComponent::computeLayout()
|
|||
newSize = sizeMaxX;
|
||||
else
|
||||
newSize = sizeMaxX.x * sizeMaxX.y >= sizeMaxY.x * sizeMaxY.y ? sizeMaxX : sizeMaxY;
|
||||
|
||||
if (image.second.getSize() != newSize)
|
||||
image.second.setResize(newSize.x, newSize.y);
|
||||
|
||||
// In case maxItemSize needs to be updated.
|
||||
if (newSize.x != sizeChange.x)
|
||||
sizeChange.x = newSize.x;
|
||||
if (newSize.y != sizeChange.y)
|
||||
sizeChange.y = newSize.y;
|
||||
}
|
||||
|
||||
if (maxItemSize.x != sizeChange.x)
|
||||
maxItemSize.x = sizeChange.x;
|
||||
if (maxItemSize.y != sizeChange.y)
|
||||
maxItemSize.y = sizeChange.y;
|
||||
|
||||
// Pre-compute layout parameters.
|
||||
float lineWidth = (mDirection == Direction::row ? (maxItemSize.y + mItemMargin.y) :
|
||||
(maxItemSize.x + mItemMargin.x));
|
||||
float anchorXStart{anchorX};
|
||||
float anchorYStart{anchorY};
|
||||
|
||||
|
@ -92,30 +150,29 @@ void FlexboxComponent::computeLayout()
|
|||
auto size{image.second.getSize()};
|
||||
|
||||
// Top-left anchor position.
|
||||
float x{anchorX - anchorOriginX * size.x};
|
||||
float y{anchorY - anchorOriginY * size.y};
|
||||
float x{anchorX};
|
||||
float y{anchorY};
|
||||
|
||||
// Apply alignment
|
||||
if (mAlign == Align::end) {
|
||||
// Apply alignment.
|
||||
if (mItemPlacement == "end") {
|
||||
x += directionLine.x == 0 ? (maxItemSize.x - size.x) : 0;
|
||||
y += directionLine.y == 0 ? (maxItemSize.y - size.y) : 0;
|
||||
}
|
||||
else if (mAlign == Align::center) {
|
||||
else if (mItemPlacement == "center") {
|
||||
x += directionLine.x == 0 ? (maxItemSize.x - size.x) / 2 : 0;
|
||||
y += directionLine.y == 0 ? (maxItemSize.y - size.y) / 2 : 0;
|
||||
}
|
||||
else if (mAlign == Align::stretch && mDirection == Direction::row) {
|
||||
else if (mItemPlacement == "stretch" && mDirection == "row") {
|
||||
image.second.setSize(image.second.getSize().x, maxItemSize.y);
|
||||
}
|
||||
|
||||
// Apply origin.
|
||||
if (mOrigin.x > 0 && mOrigin.x <= 1)
|
||||
x -= mOrigin.x * mSize.x;
|
||||
if (mOrigin.y > 0 && mOrigin.y <= 1)
|
||||
y -= mOrigin.y * mSize.y;
|
||||
// TODO: Doesn't work correctly.
|
||||
// Apply overall container alignment.
|
||||
if (mAlignment == "right")
|
||||
x += (mSize.x - size.x * grid.x) - mItemMargin.x;
|
||||
|
||||
// Store final item position.
|
||||
image.second.setPosition(getPosition().x + x, getPosition().y + y);
|
||||
image.second.setPosition(x, y);
|
||||
|
||||
// Translate anchor.
|
||||
if ((i++ + 1) % std::max(1, static_cast<int>(mItemsPerLine)) != 0) {
|
||||
|
@ -126,11 +183,11 @@ void FlexboxComponent::computeLayout()
|
|||
else {
|
||||
// Translate to first position of next line.
|
||||
if (directionRow.x == 0) {
|
||||
anchorY += lineWidth * static_cast<float>(directionRow.y);
|
||||
anchorY += size.y + mItemMargin.y;
|
||||
anchorX = anchorXStart;
|
||||
}
|
||||
else {
|
||||
anchorX += lineWidth * static_cast<float>(directionRow.x);
|
||||
anchorX += size.x + mItemMargin.x;
|
||||
anchorY = anchorYStart;
|
||||
}
|
||||
}
|
||||
|
@ -138,56 +195,3 @@ void FlexboxComponent::computeLayout()
|
|||
|
||||
mLayoutValid = true;
|
||||
}
|
||||
|
||||
void FlexboxComponent::render(const glm::mat4& parentTrans)
|
||||
{
|
||||
if (!isVisible())
|
||||
return;
|
||||
|
||||
if (!mLayoutValid)
|
||||
computeLayout();
|
||||
|
||||
for (auto& image : mImages)
|
||||
image.second.render(parentTrans);
|
||||
}
|
||||
|
||||
void FlexboxComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
||||
const std::string& view,
|
||||
const std::string& element,
|
||||
unsigned int properties)
|
||||
{
|
||||
using namespace ThemeFlags;
|
||||
|
||||
glm::vec2 scale{getParent() ? getParent()->getSize() :
|
||||
glm::vec2{static_cast<float>(Renderer::getScreenWidth()),
|
||||
static_cast<float>(Renderer::getScreenHeight())}};
|
||||
|
||||
const ThemeData::ThemeElement* elem = theme->getElement(view, element, "badges");
|
||||
if (!elem)
|
||||
return;
|
||||
|
||||
if (properties & DIRECTION && elem->has("direction"))
|
||||
mDirection =
|
||||
elem->get<std::string>("direction") == "row" ? Direction::row : Direction::column;
|
||||
|
||||
if (elem->has("align")) {
|
||||
const auto a = elem->get<std::string>("align");
|
||||
mAlign = (a == "start" ?
|
||||
Align::start :
|
||||
(a == "end" ? Align::end : (a == "center" ? Align::center : Align::stretch)));
|
||||
}
|
||||
|
||||
if (elem->has("itemsPerLine"))
|
||||
mItemsPerLine = elem->get<float>("itemsPerLine");
|
||||
|
||||
if (elem->has("lines"))
|
||||
mLines = elem->get<float>("lines");
|
||||
|
||||
if (elem->has("itemMargin"))
|
||||
mItemMargin = elem->get<glm::vec2>("itemMargin") * scale;
|
||||
|
||||
GuiComponent::applyTheme(theme, view, element, properties);
|
||||
|
||||
// Layout no longer valid.
|
||||
mLayoutValid = false;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
// FlexboxComponent.h
|
||||
//
|
||||
// Flexbox layout component.
|
||||
// Used by gamelist views.
|
||||
//
|
||||
|
||||
#ifndef ES_CORE_COMPONENTS_FLEXBOX_COMPONENT_H
|
||||
|
@ -16,26 +15,21 @@
|
|||
class FlexboxComponent : public GuiComponent
|
||||
{
|
||||
public:
|
||||
enum class Direction : char {
|
||||
row, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
|
||||
column
|
||||
};
|
||||
FlexboxComponent(Window* window, std::vector<std::pair<std::string, ImageComponent>>& images);
|
||||
|
||||
enum class Align : char {
|
||||
start, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
|
||||
end,
|
||||
center,
|
||||
stretch
|
||||
};
|
||||
|
||||
explicit FlexboxComponent(Window* window,
|
||||
std::vector<std::pair<std::string, ImageComponent>>& images);
|
||||
|
||||
// Getters/Setters for rendering options.
|
||||
Align getAlign() const { return mAlign; }
|
||||
void setAlign(Align value)
|
||||
// Getters/setters for the layout.
|
||||
std::string getDirection() const { return mDirection; }
|
||||
void setDirection(const std::string& direction)
|
||||
{
|
||||
mAlign = value;
|
||||
assert(direction == "row" || direction == "column");
|
||||
mDirection = direction;
|
||||
}
|
||||
|
||||
std::string getAlignment() const { return mAlignment; }
|
||||
void setAlignment(const std::string& value)
|
||||
{
|
||||
assert(value == "left" || value == "right");
|
||||
mAlignment = value;
|
||||
mLayoutValid = false;
|
||||
}
|
||||
|
||||
|
@ -53,34 +47,39 @@ public:
|
|||
mLayoutValid = false;
|
||||
}
|
||||
|
||||
std::string getItemPlacement() const { return mItemPlacement; }
|
||||
void setItemPlacement(const std::string& value)
|
||||
{
|
||||
assert(value == "start" || value == "center" || value == "end" || value == "stretch");
|
||||
mItemPlacement = value;
|
||||
mLayoutValid = false;
|
||||
}
|
||||
|
||||
glm::vec2 getItemMargin() const { return mItemMargin; }
|
||||
void setItemMargin(glm::vec2 value)
|
||||
{
|
||||
mItemMargin = value;
|
||||
mItemMargin.x = std::roundf(value.x * Renderer::getScreenWidth());
|
||||
mItemMargin.y = std::roundf(value.y * Renderer::getScreenHeight());
|
||||
mLayoutValid = false;
|
||||
}
|
||||
|
||||
void onSizeChanged() override { mLayoutValid = false; }
|
||||
void render(const glm::mat4& parentTrans) override;
|
||||
void applyTheme(const std::shared_ptr<ThemeData>& theme,
|
||||
const std::string& view,
|
||||
const std::string& element,
|
||||
unsigned int properties) override;
|
||||
|
||||
private:
|
||||
// Calculate flexbox layout.
|
||||
void computeLayout();
|
||||
|
||||
// Layout options.
|
||||
Direction mDirection;
|
||||
Align mAlign;
|
||||
|
||||
std::vector<std::pair<std::string, ImageComponent>>& mImages;
|
||||
|
||||
// Layout options.
|
||||
std::string mDirection;
|
||||
std::string mAlignment;
|
||||
unsigned int mItemsPerLine;
|
||||
unsigned int mLines;
|
||||
|
||||
std::string mItemPlacement;
|
||||
glm::vec2 mItemMargin;
|
||||
|
||||
bool mLayoutValid;
|
||||
};
|
||||
|
||||
|
|
|
@ -179,11 +179,6 @@ std::vector<HelpPrompt> GuiMsgBox::getHelpPrompts()
|
|||
{
|
||||
std::vector<HelpPrompt> prompts = mGrid.getHelpPrompts();
|
||||
|
||||
// If there is only one button, then remove the "Choose" help symbol
|
||||
// as there is no way to make a choice.
|
||||
if (mButtons.size() == 1)
|
||||
prompts.pop_back();
|
||||
|
||||
if (!mDisableBackButton)
|
||||
prompts.push_back(HelpPrompt("b", "Back"));
|
||||
|
||||
|
|
|
@ -240,10 +240,9 @@ based on: 'recalbox-multi' by the Recalbox community
|
|||
<pos>0.8125 0.675</pos>
|
||||
<size>0.15 0.21</size>
|
||||
<origin>0 0</origin>
|
||||
<direction>row</direction>
|
||||
<align>start</align>
|
||||
<itemsPerLine>3</itemsPerLine>
|
||||
<lines>2</lines>
|
||||
<alignment>left</alignment>
|
||||
<itemsPerRow>3</itemsPerRow>
|
||||
<rows>2</rows>
|
||||
<itemMargin>0.0028125 0.005</itemMargin>
|
||||
<slots>favorite completed kidgame broken altemulator</slots>
|
||||
</badges>
|
||||
|
|
Loading…
Reference in a new issue