mirror of
https://github.com/RetroDECK/ES-DE.git
synced 2024-11-21 21:55:38 +00:00
Moved game counting to the sort function and improved the custom collection handling.
This commit is contained in:
parent
c5ecfb4e36
commit
1c831249da
4
NEWS.md
4
NEWS.md
|
@ -23,7 +23,9 @@ Many bugs have been fixed, and numerous features that were only partially implem
|
|||
* Updated scraper to support additional media files, detailed configuration of what to scrape, semi-automatic mode etc.
|
||||
* In the metadata editor, any values updated by the single-game scraper or by the user are now highlighted using a different font color
|
||||
* Files or folders can now be flagged for exclusion when scraping with the multi-scraper, and for folders it can be set to apply recursively
|
||||
* Gamelist sorting now working as expected and is persistent throughout the application session
|
||||
* Gamelist sorting is now working as expected and is persistent throughout the application session
|
||||
* Game counting is now done during sorting instead of every time a system is selected. This should make the UI more responsive in case of large game libraries
|
||||
* Added a system view counter for favorite games in addition to the total number of games
|
||||
* Added support for jumping to the start and end of gamelists and menus using the controller trigger buttons (or equivalent keyboard mappings)
|
||||
* Full navigation sound support, configurable per theme with a fallback to the built-in sounds if the theme does not support it
|
||||
* New default theme rbsimple-DE bundled with the software, this theme is largely based on recalbox-multi by the Recalbox community
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// EmulationStation Desktop Edition
|
||||
// CollectionSystemManager.cpp
|
||||
//
|
||||
// Manages collections of the following two types:
|
||||
|
@ -30,8 +32,9 @@
|
|||
#include "Settings.h"
|
||||
#include "SystemData.h"
|
||||
#include "ThemeData.h"
|
||||
#include <pugixml.hpp>
|
||||
|
||||
#include <fstream>
|
||||
#include <pugixml.hpp>
|
||||
|
||||
std::string myCollectionsName = "collections";
|
||||
|
||||
|
@ -237,8 +240,7 @@ void CollectionSystemManager::updateSystemsList()
|
|||
}
|
||||
|
||||
// If we were editing a custom collection, and it's no longer enabled, exit edit mode.
|
||||
if (mIsEditingCustom && !mEditingCollectionSystemData->isEnabled)
|
||||
{
|
||||
if (mIsEditingCustom && !mEditingCollectionSystemData->isEnabled) {
|
||||
exitEditMode();
|
||||
}
|
||||
}
|
||||
|
@ -385,11 +387,15 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS
|
|||
if (sysData.decl.isCustom &&
|
||||
Settings::getInstance()->getBool("UseCustomCollectionsSystem")) {
|
||||
// In case of a returned null pointer, we know there is no parent.
|
||||
if (rootFolder->getParent() == nullptr)
|
||||
if (rootFolder->getParent() == nullptr) {
|
||||
ViewController::get()->onFileChanged(rootFolder, FILE_METADATA_CHANGED);
|
||||
else
|
||||
}
|
||||
else {
|
||||
rootFolder->getParent()->sort(rootFolder->getSortTypeFromString(
|
||||
rootFolder->getSortTypeString()), mFavoritesSorting);
|
||||
ViewController::get()->onFileChanged(
|
||||
rootFolder->getParent(), FILE_METADATA_CHANGED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -591,6 +597,9 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file)
|
|||
|
||||
ViewController::get()->getGameListView(systemViewToUpdate).get()->
|
||||
remove(collectionEntry, false);
|
||||
systemViewToUpdate->getRootFolder()->sort(rootFolder->getSortTypeFromString(
|
||||
rootFolder->getSortTypeString()),
|
||||
Settings::getInstance()->getBool("FavFirstCustom"));
|
||||
}
|
||||
else {
|
||||
// We didn't find it here, so we should add it.
|
||||
|
@ -601,10 +610,6 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file)
|
|||
onFileChanged(newGame, FILE_METADATA_CHANGED);
|
||||
if (name == "recent")
|
||||
rootFolder->sort(rootFolder->getSortTypeFromString("last played, descending"));
|
||||
else
|
||||
rootFolder->sort(rootFolder->getSortTypeFromString(
|
||||
rootFolder->getSortTypeString()),
|
||||
Settings::getInstance()->getBool("FavFirstCustom"));
|
||||
|
||||
ViewController::get()->onFileChanged(systemViewToUpdate->
|
||||
getRootFolder(), FILE_SORTED);
|
||||
|
@ -612,6 +617,7 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file)
|
|||
// Add to bundle index as well, if needed.
|
||||
if (systemViewToUpdate != sysData)
|
||||
systemViewToUpdate->getIndex()->addToIndex(newGame);
|
||||
refreshCollectionSystems(newGame);
|
||||
}
|
||||
updateCollectionFolderMetadata(sysData);
|
||||
}
|
||||
|
@ -783,15 +789,15 @@ SystemData* CollectionSystemManager::addNewCustomCollection(std::string name)
|
|||
decl.name = name;
|
||||
decl.longName = name;
|
||||
|
||||
return createNewCollectionEntry(name, decl);
|
||||
return createNewCollectionEntry(name, decl, true, true);
|
||||
}
|
||||
|
||||
// Create a new empty collection system based on the name and declaration.
|
||||
SystemData* CollectionSystemManager::createNewCollectionEntry(
|
||||
std::string name, CollectionSystemDecl sysDecl, bool index)
|
||||
std::string name, CollectionSystemDecl sysDecl, bool index, bool custom)
|
||||
{
|
||||
SystemData* newSys = new SystemData(
|
||||
name, sysDecl.longName, mCollectionEnvData, sysDecl.themeFolder, true);
|
||||
name, sysDecl.longName, mCollectionEnvData, sysDecl.themeFolder, true, custom);
|
||||
|
||||
CollectionSystemData newCollectionData;
|
||||
newCollectionData.system = newSys;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// EmulationStation Desktop Edition
|
||||
// CollectionSystemManager.h
|
||||
//
|
||||
// Manages collections of the following two types:
|
||||
|
@ -17,7 +19,6 @@
|
|||
// the required re-sort and refresh of the gamelists.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#ifndef ES_APP_COLLECTION_SYSTEM_MANAGER_H
|
||||
#define ES_APP_COLLECTION_SYSTEM_MANAGER_H
|
||||
|
||||
|
@ -111,7 +112,7 @@ private:
|
|||
void initCustomCollectionSystems();
|
||||
SystemData* getAllGamesCollection();
|
||||
SystemData* createNewCollectionEntry(std::string name,
|
||||
CollectionSystemDecl sysDecl, bool index = true);
|
||||
CollectionSystemDecl sysDecl, bool index = true, bool custom = false);
|
||||
void populateAutoCollection(CollectionSystemData* sysData);
|
||||
void populateCustomCollection(CollectionSystemData* sysData);
|
||||
|
||||
|
@ -126,9 +127,7 @@ private:
|
|||
std::vector<std::string> getUserCollectionThemeFolders();
|
||||
|
||||
void trimCollectionCount(FileData* rootFolder, int limit);
|
||||
|
||||
bool themeFolderExists(std::string folder);
|
||||
|
||||
bool includeFileInAutoCollections(FileData* file);
|
||||
|
||||
SystemData* mCustomCollectionsBundle;
|
||||
|
|
|
@ -436,7 +436,8 @@ void FileData::removeChild(FileData* file)
|
|||
assert(false);
|
||||
}
|
||||
|
||||
void FileData::sort(ComparisonFunction& comparator, bool ascending)
|
||||
void FileData::sort(ComparisonFunction& comparator, bool ascending,
|
||||
std::pair<unsigned int, unsigned int>& gameCount)
|
||||
{
|
||||
mFirstLetterIndex.clear();
|
||||
mOnlyFolders = true;
|
||||
|
@ -507,6 +508,14 @@ void FileData::sort(ComparisonFunction& comparator, bool ascending)
|
|||
}
|
||||
|
||||
for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) {
|
||||
// Game count, which will be displayed in the system view.
|
||||
if ((*it)->getType() == GAME && (*it)->getCountAsGame()) {
|
||||
if (!(*it)->getFavorite())
|
||||
gameCount.first++;
|
||||
else
|
||||
gameCount.second++;
|
||||
}
|
||||
|
||||
if ((*it)->getType() != FOLDER)
|
||||
mOnlyFolders = false;
|
||||
|
||||
|
@ -517,7 +526,7 @@ void FileData::sort(ComparisonFunction& comparator, bool ascending)
|
|||
}
|
||||
// Iterate through any child folders.
|
||||
if ((*it)->getChildren().size() > 0)
|
||||
(*it)->sort(comparator, ascending);
|
||||
(*it)->sort(comparator, ascending, gameCount);
|
||||
}
|
||||
|
||||
// If there are only folders in the gamelist, then it makes sense to still
|
||||
|
@ -540,7 +549,8 @@ void FileData::sort(ComparisonFunction& comparator, bool ascending)
|
|||
mFirstLetterIndex.insert(mFirstLetterIndex.begin(), FOLDER_CHAR);
|
||||
}
|
||||
|
||||
void FileData::sortFavoritesOnTop(ComparisonFunction& comparator, bool ascending)
|
||||
void FileData::sortFavoritesOnTop(ComparisonFunction& comparator, bool ascending,
|
||||
std::pair<unsigned int, unsigned int>& gameCount)
|
||||
{
|
||||
mFirstLetterIndex.clear();
|
||||
mOnlyFolders = true;
|
||||
|
@ -560,6 +570,14 @@ void FileData::sortFavoritesOnTop(ComparisonFunction& comparator, bool ascending
|
|||
continue;
|
||||
}
|
||||
|
||||
// Game count, which will be displayed in the system view.
|
||||
if (mChildren[i]->getType() == GAME && mChildren[i]->getCountAsGame()) {
|
||||
if (!mChildren[i]->getFavorite())
|
||||
gameCount.first++;
|
||||
else
|
||||
gameCount.second++;
|
||||
}
|
||||
|
||||
if (foldersOnTop && mChildren[i]->getType() == FOLDER) {
|
||||
if (!mChildren[i]->getFavorite())
|
||||
mChildrenFolders.push_back(mChildren[i]);
|
||||
|
@ -664,13 +682,13 @@ void FileData::sortFavoritesOnTop(ComparisonFunction& comparator, bool ascending
|
|||
for (auto it = mChildrenFavoritesFolders.cbegin(); it !=
|
||||
mChildrenFavoritesFolders.cend(); it++) {
|
||||
if ((*it)->getChildren().size() > 0)
|
||||
(*it)->sortFavoritesOnTop(comparator, ascending);
|
||||
(*it)->sortFavoritesOnTop(comparator, ascending, gameCount);
|
||||
}
|
||||
|
||||
// Iterate through any child folders.
|
||||
for (auto it = mChildrenFolders.cbegin(); it != mChildrenFolders.cend(); it++) {
|
||||
if ((*it)->getChildren().size() > 0)
|
||||
(*it)->sortFavoritesOnTop(comparator, ascending);
|
||||
(*it)->sortFavoritesOnTop(comparator, ascending, gameCount);
|
||||
}
|
||||
|
||||
if (!ascending) {
|
||||
|
@ -695,10 +713,12 @@ void FileData::sortFavoritesOnTop(ComparisonFunction& comparator, bool ascending
|
|||
|
||||
void FileData::sort(const SortType& type, bool mFavoritesOnTop)
|
||||
{
|
||||
mGameCount = std::make_pair(0, 0);
|
||||
|
||||
if (mFavoritesOnTop)
|
||||
sortFavoritesOnTop(*type.comparisonFunction, type.ascending);
|
||||
sortFavoritesOnTop(*type.comparisonFunction, type.ascending, mGameCount);
|
||||
else
|
||||
sort(*type.comparisonFunction, type.ascending);
|
||||
sort(*type.comparisonFunction, type.ascending, mGameCount);
|
||||
}
|
||||
|
||||
FileData::SortType FileData::getSortTypeFromString(std::string desc) {
|
||||
|
|
|
@ -51,6 +51,7 @@ public:
|
|||
const bool getFavorite();
|
||||
const bool getHidden();
|
||||
const bool getCountAsGame();
|
||||
const std::pair<unsigned int, unsigned int> getGameCount() { return mGameCount; };
|
||||
const bool getExcludeFromScraper();
|
||||
const std::vector<FileData*> getChildrenRecursive() const;
|
||||
inline FileType getType() const { return mType; }
|
||||
|
@ -123,8 +124,10 @@ public:
|
|||
description(sortDescription) {}
|
||||
};
|
||||
|
||||
void sort(ComparisonFunction& comparator, bool ascending = true);
|
||||
void sortFavoritesOnTop(ComparisonFunction& comparator, bool ascending = true);
|
||||
void sort(ComparisonFunction& comparator, bool ascending,
|
||||
std::pair<unsigned int, unsigned int>& gameCount);
|
||||
void sortFavoritesOnTop(ComparisonFunction& comparator, bool ascending,
|
||||
std::pair<unsigned int, unsigned int>& gameCount);
|
||||
void sort(const SortType& type, bool mFavoritesOnTop = false);
|
||||
MetaDataList metadata;
|
||||
|
||||
|
@ -151,6 +154,8 @@ private:
|
|||
std::vector<FileData*> mChildren;
|
||||
std::vector<FileData*> mFilteredChildren;
|
||||
std::vector<std::string> mFirstLetterIndex;
|
||||
// The pair includes non-favorite games, and favorite games.
|
||||
std::pair<unsigned int, unsigned int> mGameCount;
|
||||
bool mOnlyFolders;
|
||||
// Used for flagging a game for deletion from its gamelist.xml file.
|
||||
bool mDeletionFlag;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// EmulationStation Desktop Edition
|
||||
// SystemData.cpp
|
||||
//
|
||||
// Provides data structures for the game systems and populates and indexes them based
|
||||
|
@ -12,6 +14,7 @@
|
|||
#include "resources/ResourceManager.h"
|
||||
#include "utils/FileSystemUtil.h"
|
||||
#include "utils/StringUtil.h"
|
||||
#include "views/UIModeController.h"
|
||||
#include "CollectionSystemManager.h"
|
||||
#include "FileFilterIndex.h"
|
||||
#include "FileSorts.h"
|
||||
|
@ -20,10 +23,9 @@
|
|||
#include "Platform.h"
|
||||
#include "Settings.h"
|
||||
#include "ThemeData.h"
|
||||
#include "views/UIModeController.h"
|
||||
|
||||
#include <pugixml.hpp>
|
||||
#include <fstream>
|
||||
#include <pugixml.hpp>
|
||||
|
||||
std::vector<SystemData*> SystemData::sSystemVector;
|
||||
|
||||
|
@ -32,12 +34,14 @@ SystemData::SystemData(
|
|||
const std::string& fullName,
|
||||
SystemEnvironmentData* envData,
|
||||
const std::string& themeFolder,
|
||||
bool CollectionSystem)
|
||||
bool CollectionSystem,
|
||||
bool CustomCollectionSystem)
|
||||
: mName(name),
|
||||
mFullName(fullName),
|
||||
mEnvData(envData),
|
||||
mThemeFolder(themeFolder),
|
||||
mIsCollectionSystem(CollectionSystem),
|
||||
mIsCustomCollectionSystem(CustomCollectionSystem),
|
||||
mIsGameSystem(true),
|
||||
mScrapeFlag(false)
|
||||
{
|
||||
|
@ -402,9 +406,11 @@ std::string SystemData::getConfigPath(bool forWrite)
|
|||
|
||||
bool SystemData::isVisible()
|
||||
{
|
||||
return (getDisplayedGameCount() > 0 ||
|
||||
(UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) ||
|
||||
(mIsCollectionSystem && mName == "favorites"));
|
||||
// This function doesn't make much sense at the moment; if a game system does not have any
|
||||
// games available, it will not be processed during startup and will as such not exist.
|
||||
// In the future this function may be used for an option to hide specific systems, but
|
||||
// for the time being all systems will always be visible.
|
||||
return true;
|
||||
}
|
||||
|
||||
SystemData* SystemData::getNext() const
|
||||
|
@ -488,11 +494,6 @@ bool SystemData::hasGamelist() const
|
|||
return (Utils::FileSystem::exists(getGamelistPath(false)));
|
||||
}
|
||||
|
||||
unsigned int SystemData::getGameCount() const
|
||||
{
|
||||
return (unsigned int)mRootFolder->getFilesRecursive(GAME).size();
|
||||
}
|
||||
|
||||
SystemData* SystemData::getRandomSystem(const SystemData* currentSystem)
|
||||
{
|
||||
unsigned int total = 0;
|
||||
|
@ -612,10 +613,12 @@ FileData* SystemData::getRandomGame(const FileData* currentGame)
|
|||
return gameList.at(target);
|
||||
}
|
||||
|
||||
unsigned int SystemData::getDisplayedGameCount() const
|
||||
std::pair<unsigned int, unsigned int> SystemData::getDisplayedGameCount() const
|
||||
{
|
||||
// Pass the flag to only count games that are marked with the flag 'countasgame'.
|
||||
return (unsigned int)mRootFolder->getFilesRecursive(GAME, true, false).size();
|
||||
// Return all games for the system which are marked as 'countasgame'. As this flag is set
|
||||
// by default, normally most games will be included in the number returned from here.
|
||||
// The actual game counting takes place in FileData during sorting.
|
||||
return mRootFolder->getGameCount();
|
||||
}
|
||||
|
||||
void SystemData::loadTheme()
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// EmulationStation Desktop Edition
|
||||
// SystemData.h
|
||||
//
|
||||
// Provides data structures for the game systems and populates and indexes them based
|
||||
|
@ -7,7 +9,6 @@
|
|||
// loading.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#ifndef ES_APP_SYSTEM_DATA_H
|
||||
#define ES_APP_SYSTEM_DATA_H
|
||||
|
||||
|
@ -36,7 +37,8 @@ public:
|
|||
const std::string& fullName,
|
||||
SystemEnvironmentData* envData,
|
||||
const std::string& themeFolder,
|
||||
bool CollectionSystem = false);
|
||||
bool CollectionSystem = false,
|
||||
bool CustomCollectionSystem = false);
|
||||
|
||||
~SystemData();
|
||||
|
||||
|
@ -60,8 +62,7 @@ public:
|
|||
bool hasGamelist() const;
|
||||
std::string getThemePath() const;
|
||||
|
||||
unsigned int getGameCount() const;
|
||||
unsigned int getDisplayedGameCount() const;
|
||||
std::pair<unsigned int, unsigned int> getDisplayedGameCount() const;
|
||||
bool getScrapeFlag() { return mScrapeFlag; };
|
||||
void setScrapeFlag(bool scrapeflag) { mScrapeFlag = scrapeflag; }
|
||||
|
||||
|
@ -80,6 +81,7 @@ public:
|
|||
inline std::vector<SystemData*>::const_reverse_iterator getRevIterator() const
|
||||
{ return std::find(sSystemVector.crbegin(), sSystemVector.crend(), this); };
|
||||
inline bool isCollection() { return mIsCollectionSystem; };
|
||||
inline bool isCustomCollection() { return mIsCustomCollectionSystem; };
|
||||
inline bool isGameSystem() { return mIsGameSystem; };
|
||||
|
||||
bool isVisible();
|
||||
|
@ -101,6 +103,7 @@ public:
|
|||
|
||||
private:
|
||||
bool mIsCollectionSystem;
|
||||
bool mIsCustomCollectionSystem;
|
||||
bool mIsGameSystem;
|
||||
bool mScrapeFlag; // Only used by scraper GUI to remember which systems to scrape.
|
||||
std::string mName;
|
||||
|
|
|
@ -283,16 +283,25 @@ void SystemView::onCursorChanged(const CursorState& /*state*/)
|
|||
Math::lerp(infoStartOpacity, 0.f, t) * 255));
|
||||
}, static_cast<int>(infoStartOpacity * (goFast ? 10 : 150)));
|
||||
|
||||
unsigned int gameCount = getSelected()->getDisplayedGameCount();
|
||||
std::pair<unsigned int, unsigned int> gameCount = getSelected()->getDisplayedGameCount();
|
||||
|
||||
// Also change the text after we've fully faded out.
|
||||
setAnimation(infoFadeOut, 0, [this, gameCount] {
|
||||
std::stringstream ss;
|
||||
unsigned int totalGameCount = gameCount.first + gameCount.second;
|
||||
|
||||
if (!getSelected()->isGameSystem())
|
||||
ss << "CONFIGURATION";
|
||||
else if (getSelected()->isCollection() && (getSelected()->getName() == "favorites"))
|
||||
ss << totalGameCount << " GAME" << (totalGameCount == 1 ? " " : "S");
|
||||
// The 'recent' gamelist has probably been trimmed after sorting, so we'll cap it at
|
||||
// its maximum limit of 50 games.
|
||||
else if (getSelected()->isCollection() && (getSelected()->getName() == "recent"))
|
||||
ss << (totalGameCount > 50 ? 50 : totalGameCount) << " GAME" <<
|
||||
(totalGameCount == 1 ? " " : "S");
|
||||
else
|
||||
ss << gameCount << " GAMES AVAILABLE";
|
||||
ss << totalGameCount << " GAME" << (totalGameCount == 1 ? " " : "S ") << "(" <<
|
||||
gameCount.second << " FAVORITE" << (gameCount.second == 1 ? ")" : "S)");
|
||||
|
||||
mSystemInfo.setText(ss.str());
|
||||
}, false, 1);
|
||||
|
|
|
@ -200,7 +200,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input)
|
|||
else
|
||||
favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst");
|
||||
|
||||
if (favoritesSorting && static_cast<std::string>(getName()) != "recent") {
|
||||
if (favoritesSorting && static_cast<std::string>(
|
||||
mRoot->getSystem()->getName()) != "recent") {
|
||||
FileData* entryToSelect;
|
||||
// Add favorite flag.
|
||||
if (!getCursor()->getFavorite()) {
|
||||
|
@ -300,9 +301,11 @@ bool ISimpleGameListView::input(InputConfig* config, Input input)
|
|||
}
|
||||
else if (CollectionSystemManager::get()->toggleGameInCollection(entryToUpdate)) {
|
||||
// Jump to the first entry in the gamelist if the last favorite was unmarked.
|
||||
if (foldersOnTop && removedLastFavorite)
|
||||
if (foldersOnTop && removedLastFavorite &&
|
||||
!entryToUpdate->getSystem()->isCustomCollection())
|
||||
setCursor(getFirstGameEntry());
|
||||
else if (removedLastFavorite)
|
||||
else if (removedLastFavorite &&
|
||||
!entryToUpdate->getSystem()->isCustomCollection())
|
||||
setCursor(getFirstEntry());
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
#ifndef ES_APP_VIEWS_GAME_LIST_ISIMPLE_GAME_LIST_VIEW_H
|
||||
#define ES_APP_VIEWS_GAME_LIST_ISIMPLE_GAME_LIST_VIEW_H
|
||||
|
||||
#include "views/gamelist/IGameListView.h"
|
||||
#include "components/ImageComponent.h"
|
||||
#include "components/TextComponent.h"
|
||||
#include "views/gamelist/IGameListView.h"
|
||||
|
||||
#include <stack>
|
||||
|
||||
|
@ -46,7 +46,6 @@ protected:
|
|||
ImageComponent mBackground;
|
||||
|
||||
std::vector<GuiComponent*> mThemeExtras;
|
||||
|
||||
std::stack<FileData*> mCursorStack;
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue