mirror of
https://github.com/RetroDECK/ES-DE.git
synced 2024-11-25 07:35:38 +00:00
Changed 'emulationstation' folder to 'es-app' because I forgot executables don't have extensions on Linux
Half of the ES code has been missing for 5 days because I am incompetent
This commit is contained in:
parent
dbdbcde6cd
commit
bc72990f39
|
@ -185,4 +185,4 @@ set(LIBRARY_OUTPUT_PATH ${dir} CACHE PATH "Build directory" FORCE)
|
|||
|
||||
add_subdirectory("external")
|
||||
add_subdirectory("es-core")
|
||||
add_subdirectory("emulationstation")
|
||||
add_subdirectory("es-app")
|
||||
|
|
149
es-app/CMakeLists.txt
Normal file
149
es-app/CMakeLists.txt
Normal file
|
@ -0,0 +1,149 @@
|
|||
project("emulationstation")
|
||||
|
||||
set(ES_HEADERS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/EmulationStation.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/FileData.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/FileSorts.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/MetaData.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/PlatformId.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/ScraperCmdLine.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Gamelist.h
|
||||
|
||||
# GuiComponents
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/RatingComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ScraperSearchComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextListComponent.h
|
||||
|
||||
# Guis
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiFastSelect.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMetaDataEd.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGameScraper.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistOptions.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMenu.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSettings.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.h
|
||||
|
||||
# Scrapers
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBScraper.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/TheArchiveScraper.h
|
||||
|
||||
# Views
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/BasicGameListView.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/DetailedGameListView.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/IGameListView.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/ISimpleGameListView.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/GridGameListView.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/views/SystemView.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.h
|
||||
|
||||
# Animations
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/animations/LaunchAnimation.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/animations/MoveCameraAnimation.h
|
||||
)
|
||||
|
||||
set(ES_SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/FileData.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/FileSorts.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/MameNameMap.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/MetaData.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/PlatformId.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/ScraperCmdLine.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Gamelist.cpp
|
||||
|
||||
# GuiComponents
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/RatingComponent.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ScraperSearchComponent.cpp
|
||||
|
||||
# Guis
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiFastSelect.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMetaDataEd.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGameScraper.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistOptions.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMenu.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSettings.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.cpp
|
||||
|
||||
# Scrapers
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBScraper.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/TheArchiveScraper.cpp
|
||||
|
||||
# Views
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/BasicGameListView.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/DetailedGameListView.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/IGameListView.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/ISimpleGameListView.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/GridGameListView.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/views/SystemView.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.cpp
|
||||
)
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# define OS specific sources and headers
|
||||
if(MSVC)
|
||||
LIST(APPEND ES_SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/EmulationStation.rc
|
||||
)
|
||||
endif()
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# define target
|
||||
include_directories(${COMMON_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
add_executable(emulationstation ${ES_SOURCES} ${ES_HEADERS})
|
||||
target_link_libraries(emulationstation ${COMMON_LIBRARIES} es-core)
|
||||
|
||||
# special properties for Windows builds
|
||||
if(MSVC)
|
||||
#show console in debug builds, but not in proper release builds
|
||||
#Note that up to CMake 2.8.10 this feature is broken: http://public.kitware.com/Bug/view.php?id=12566
|
||||
set_target_properties(emulationstation PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
|
||||
set_target_properties(emulationstation PROPERTIES COMPILE_DEFINITIONS_DEBUG "_CONSOLE")
|
||||
set_target_properties(emulationstation PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:CONSOLE")
|
||||
set_target_properties(emulationstation PROPERTIES COMPILE_DEFINITIONS_RELWITHDEBINFO "_CONSOLE")
|
||||
set_target_properties(emulationstation PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS")
|
||||
set_target_properties(emulationstation PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS")
|
||||
endif()
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# set up CPack install stuff so `make install` does something useful
|
||||
|
||||
install(TARGETS emulationstation
|
||||
RUNTIME
|
||||
DESTINATION bin)
|
||||
|
||||
INCLUDE(InstallRequiredSystemLibraries)
|
||||
|
||||
SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A flexible graphical emulator front-end")
|
||||
SET(CPACK_PACKAGE_DESCRIPTION "EmulationStation is a flexible, graphical front-end designed for keyboardless navigation of your multi-platform retro game collection.")
|
||||
|
||||
SET(CPACK_RESOURCE_FILE LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE.md")
|
||||
SET(CPACK_RESOURCE_FILE README "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
|
||||
|
||||
SET(CPACK_DEBIAN_PACKAGE_MAINTAINER "Alec Lofquist <allofquist@yahoo.com>")
|
||||
SET(CPACK_DEBIAN_PACKAGE_SECTION "misc")
|
||||
SET(CPACK_DEBIAN_PACKAGE_PRIORITY "extra")
|
||||
SET(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libsdl2-2.0-0, libboost-system1.54.0, libboost-filesystem1.54.0, libfreeimage3, libfreetype6, libcurl3, libasound2")
|
||||
SET(CPACK_DEBIAN_PACKAGE_BUILDS_DEPENDS "debhelper (>= 8.0.0), cmake, g++ (>= 4.8), libsdl2-dev, libboost-system-dev, libboost-filesystem-dev, libboost-date-time-dev, libfreeimage-dev, libfreetype6-dev, libeigen3-dev, libcurl4-openssl-dev, libasound2-dev, libgl1-mesa-dev")
|
||||
|
||||
SET(CPACK_PACKAGE_VENDOR "emulationstation.org")
|
||||
SET(CPACK_PACKAGE_VERSION "2.0.0~rc1")
|
||||
SET(CPACK_PACKAGE_VERSION_MAJOR "2")
|
||||
SET(CPACK_PACKAGE_VERSION_MINOR "0")
|
||||
SET(CPACK_PACKAGE_VERSION_PATCH "0")
|
||||
SET(CPACK_PACKAGE_INSTALL_DIRECTORY "emulationstation_${CMAKE_PACKAGE_VERSION}")
|
||||
SET(CPACK_PACKAGE_EXECUTABLES "emulationstation" "emulationstation")
|
||||
|
||||
SET(CPACK_GENERATOR "TGZ;DEB")
|
||||
|
||||
INCLUDE(CPack)
|
BIN
es-app/src/EmulationStation.aps
Normal file
BIN
es-app/src/EmulationStation.aps
Normal file
Binary file not shown.
13
es-app/src/EmulationStation.h
Normal file
13
es-app/src/EmulationStation.h
Normal file
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
// These numbers and strings need to be manually updated for a new version.
|
||||
// Do this version number update as the very last commit for the new release version.
|
||||
#define PROGRAM_VERSION_MAJOR 2
|
||||
#define PROGRAM_VERSION_MINOR 0
|
||||
#define PROGRAM_VERSION_MAINTENANCE 0
|
||||
#define PROGRAM_VERSION_STRING "2.0.0-rc1"
|
||||
|
||||
#define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__
|
||||
|
||||
#define RESOURCE_VERSION_STRING "2,0,0\0"
|
||||
#define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE
|
39
es-app/src/EmulationStation.rc
Normal file
39
es-app/src/EmulationStation.rc
Normal file
|
@ -0,0 +1,39 @@
|
|||
#include "EmulationStation.h"
|
||||
|
||||
#include "windows.h"
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION RESOURCE_VERSION
|
||||
PRODUCTVERSION RESOURCE_VERSION
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904E4"
|
||||
BEGIN
|
||||
VALUE "Comments", "\0"
|
||||
VALUE "FileDescription", "EmulationStation - emulator frontend\0"
|
||||
VALUE "FileVersion", RESOURCE_VERSION_STRING
|
||||
VALUE "InternalName", "emulationstation.exe\0"
|
||||
VALUE "LegalCopyright", "\0"
|
||||
VALUE "LegalTrademarks", "\0"
|
||||
VALUE "OriginalFilename", "emulationstation.exe\0"
|
||||
VALUE "ProductName", "EmulationStation\0"
|
||||
VALUE "ProductVersion", PROGRAM_VERSION_STRING
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1252
|
||||
END
|
||||
END
|
||||
|
||||
IDI_ES_LOGO ICON DISCARDABLE "../data/es_icon.ico"
|
144
es-app/src/FileData.cpp
Normal file
144
es-app/src/FileData.cpp
Normal file
|
@ -0,0 +1,144 @@
|
|||
#include "FileData.h"
|
||||
#include "SystemData.h"
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
std::string removeParenthesis(const std::string& str)
|
||||
{
|
||||
// remove anything in parenthesis or brackets
|
||||
// should be roughly equivalent to the regex replace "\((.*)\)|\[(.*)\]" with ""
|
||||
// I would love to just use regex, but it's not worth pulling in another boost lib for one function that is used once
|
||||
|
||||
std::string ret = str;
|
||||
size_t start, end;
|
||||
|
||||
static const int NUM_TO_REPLACE = 2;
|
||||
static const char toReplace[NUM_TO_REPLACE*2] = { '(', ')', '[', ']' };
|
||||
|
||||
bool done = false;
|
||||
while(!done)
|
||||
{
|
||||
done = true;
|
||||
for(int i = 0; i < NUM_TO_REPLACE; i++)
|
||||
{
|
||||
end = ret.find_first_of(toReplace[i*2+1]);
|
||||
start = ret.find_last_of(toReplace[i*2], end);
|
||||
|
||||
if(start != std::string::npos && end != std::string::npos)
|
||||
{
|
||||
ret.erase(start, end - start + 1);
|
||||
done = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// also strip whitespace
|
||||
end = ret.find_last_not_of(' ');
|
||||
if(end != std::string::npos)
|
||||
end++;
|
||||
|
||||
ret = ret.substr(0, end);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
FileData::FileData(FileType type, const fs::path& path, SystemData* system)
|
||||
: mType(type), mPath(path), mSystem(system), mParent(NULL), metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor!
|
||||
{
|
||||
// metadata needs at least a name field (since that's what getName() will return)
|
||||
if(metadata.get("name").empty())
|
||||
metadata.set("name", getCleanName());
|
||||
}
|
||||
|
||||
FileData::~FileData()
|
||||
{
|
||||
if(mParent)
|
||||
mParent->removeChild(this);
|
||||
|
||||
while(mChildren.size())
|
||||
delete mChildren.back();
|
||||
}
|
||||
|
||||
std::string FileData::getCleanName() const
|
||||
{
|
||||
std::string stem = mPath.stem().generic_string();
|
||||
if(mSystem && mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO))
|
||||
stem = PlatformIds::getCleanMameName(stem.c_str());
|
||||
|
||||
return removeParenthesis(stem);
|
||||
}
|
||||
|
||||
const std::string& FileData::getThumbnailPath() const
|
||||
{
|
||||
if(!metadata.get("thumbnail").empty())
|
||||
return metadata.get("thumbnail");
|
||||
else
|
||||
return metadata.get("image");
|
||||
}
|
||||
|
||||
|
||||
std::vector<FileData*> FileData::getFilesRecursive(unsigned int typeMask) const
|
||||
{
|
||||
std::vector<FileData*> out;
|
||||
|
||||
for(auto it = mChildren.begin(); it != mChildren.end(); it++)
|
||||
{
|
||||
if((*it)->getType() & typeMask)
|
||||
out.push_back(*it);
|
||||
|
||||
if((*it)->getChildren().size() > 0)
|
||||
{
|
||||
std::vector<FileData*> subchildren = (*it)->getFilesRecursive(typeMask);
|
||||
out.insert(out.end(), subchildren.cbegin(), subchildren.cend());
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void FileData::addChild(FileData* file)
|
||||
{
|
||||
assert(mType == FOLDER);
|
||||
assert(file->getParent() == NULL);
|
||||
|
||||
mChildren.push_back(file);
|
||||
file->mParent = this;
|
||||
}
|
||||
|
||||
void FileData::removeChild(FileData* file)
|
||||
{
|
||||
assert(mType == FOLDER);
|
||||
assert(file->getParent() == this);
|
||||
|
||||
for(auto it = mChildren.begin(); it != mChildren.end(); it++)
|
||||
{
|
||||
if(*it == file)
|
||||
{
|
||||
mChildren.erase(it);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// File somehow wasn't in our children.
|
||||
assert(false);
|
||||
}
|
||||
|
||||
void FileData::sort(ComparisonFunction& comparator, bool ascending)
|
||||
{
|
||||
std::sort(mChildren.begin(), mChildren.end(), comparator);
|
||||
|
||||
for(auto it = mChildren.begin(); it != mChildren.end(); it++)
|
||||
{
|
||||
if((*it)->getChildren().size() > 0)
|
||||
(*it)->sort(comparator, ascending);
|
||||
}
|
||||
|
||||
if(!ascending)
|
||||
std::reverse(mChildren.begin(), mChildren.end());
|
||||
}
|
||||
|
||||
void FileData::sort(const SortType& type)
|
||||
{
|
||||
sort(*type.comparisonFunction, type.ascending);
|
||||
}
|
77
es-app/src/FileData.h
Normal file
77
es-app/src/FileData.h
Normal file
|
@ -0,0 +1,77 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include "MetaData.h"
|
||||
|
||||
class SystemData;
|
||||
|
||||
enum FileType
|
||||
{
|
||||
GAME = 1, // Cannot have children.
|
||||
FOLDER = 2
|
||||
};
|
||||
|
||||
enum FileChangeType
|
||||
{
|
||||
FILE_ADDED,
|
||||
FILE_METADATA_CHANGED,
|
||||
FILE_REMOVED,
|
||||
FILE_SORTED
|
||||
};
|
||||
|
||||
// Used for loading/saving gamelist.xml.
|
||||
const char* fileTypeToString(FileType type);
|
||||
FileType stringToFileType(const char* str);
|
||||
|
||||
// Remove (.*) and [.*] from str
|
||||
std::string removeParenthesis(const std::string& str);
|
||||
|
||||
// A tree node that holds information for a file.
|
||||
class FileData
|
||||
{
|
||||
public:
|
||||
FileData(FileType type, const boost::filesystem::path& path, SystemData* system);
|
||||
virtual ~FileData();
|
||||
|
||||
inline const std::string& getName() const { return metadata.get("name"); }
|
||||
inline FileType getType() const { return mType; }
|
||||
inline const boost::filesystem::path& getPath() const { return mPath; }
|
||||
inline FileData* getParent() const { return mParent; }
|
||||
inline const std::vector<FileData*>& getChildren() const { return mChildren; }
|
||||
inline SystemData* getSystem() const { return mSystem; }
|
||||
|
||||
virtual const std::string& getThumbnailPath() const;
|
||||
|
||||
std::vector<FileData*> getFilesRecursive(unsigned int typeMask) const;
|
||||
|
||||
void addChild(FileData* file); // Error if mType != FOLDER
|
||||
void removeChild(FileData* file); //Error if mType != FOLDER
|
||||
|
||||
// Returns our best guess at the "real" name for this file (will strip parenthesis and attempt to perform MAME name translation)
|
||||
std::string getCleanName() const;
|
||||
|
||||
typedef bool ComparisonFunction(const FileData* a, const FileData* b);
|
||||
struct SortType
|
||||
{
|
||||
ComparisonFunction* comparisonFunction;
|
||||
bool ascending;
|
||||
std::string description;
|
||||
|
||||
SortType(ComparisonFunction* sortFunction, bool sortAscending, const std::string & sortDescription)
|
||||
: comparisonFunction(sortFunction), ascending(sortAscending), description(sortDescription) {}
|
||||
};
|
||||
|
||||
void sort(ComparisonFunction& comparator, bool ascending = true);
|
||||
void sort(const SortType& type);
|
||||
|
||||
MetaDataList metadata;
|
||||
|
||||
private:
|
||||
FileType mType;
|
||||
boost::filesystem::path mPath;
|
||||
SystemData* mSystem;
|
||||
FileData* mParent;
|
||||
std::vector<FileData*> mChildren;
|
||||
};
|
72
es-app/src/FileSorts.cpp
Normal file
72
es-app/src/FileSorts.cpp
Normal file
|
@ -0,0 +1,72 @@
|
|||
#include "FileSorts.h"
|
||||
|
||||
namespace FileSorts
|
||||
{
|
||||
const FileData::SortType typesArr[] = {
|
||||
FileData::SortType(&compareFileName, true, "filename, ascending"),
|
||||
FileData::SortType(&compareFileName, false, "filename, descending"),
|
||||
|
||||
FileData::SortType(&compareRating, true, "rating, ascending"),
|
||||
FileData::SortType(&compareRating, false, "rating, descending"),
|
||||
|
||||
FileData::SortType(&compareTimesPlayed, true, "times played, ascending"),
|
||||
FileData::SortType(&compareTimesPlayed, false, "times played, descending"),
|
||||
|
||||
FileData::SortType(&compareLastPlayed, true, "last played, ascending"),
|
||||
FileData::SortType(&compareLastPlayed, false, "last played, descending")
|
||||
};
|
||||
|
||||
const std::vector<FileData::SortType> SortTypes(typesArr, typesArr + sizeof(typesArr)/sizeof(typesArr[0]));
|
||||
|
||||
//returns if file1 should come before file2
|
||||
bool compareFileName(const FileData* file1, const FileData* file2)
|
||||
{
|
||||
std::string name1 = file1->getName();
|
||||
std::string name2 = file2->getName();
|
||||
|
||||
//min of name1/name2 .length()s
|
||||
unsigned int count = name1.length() > name2.length() ? name2.length() : name1.length();
|
||||
for(unsigned int i = 0; i < count; i++)
|
||||
{
|
||||
if(toupper(name1[i]) != toupper(name2[i]))
|
||||
{
|
||||
return toupper(name1[i]) < toupper(name2[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return name1.length() < name2.length();
|
||||
}
|
||||
|
||||
bool compareRating(const FileData* file1, const FileData* file2)
|
||||
{
|
||||
//only games have rating metadata
|
||||
if(file1->metadata.getType() == GAME_METADATA && file2->metadata.getType() == GAME_METADATA)
|
||||
{
|
||||
return file1->metadata.getFloat("rating") < file2->metadata.getFloat("rating");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool compareTimesPlayed(const FileData* file1, const FileData* file2)
|
||||
{
|
||||
//only games have playcount metadata
|
||||
if(file1->metadata.getType() == GAME_METADATA && file2->metadata.getType() == GAME_METADATA)
|
||||
{
|
||||
return (file1)->metadata.getInt("playcount") < (file2)->metadata.getInt("playcount");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool compareLastPlayed(const FileData* file1, const FileData* file2)
|
||||
{
|
||||
//only games have lastplayed metadata
|
||||
if(file1->metadata.getType() == GAME_METADATA && file2->metadata.getType() == GAME_METADATA)
|
||||
{
|
||||
return (file1)->metadata.getTime("lastplayed") < (file2)->metadata.getTime("lastplayed");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
14
es-app/src/FileSorts.h
Normal file
14
es-app/src/FileSorts.h
Normal file
|
@ -0,0 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include "FileData.h"
|
||||
|
||||
namespace FileSorts
|
||||
{
|
||||
bool compareFileName(const FileData* file1, const FileData* file2);
|
||||
bool compareRating(const FileData* file1, const FileData* file2);
|
||||
bool compareTimesPlayed(const FileData* file1, const FileData* fil2);
|
||||
bool compareLastPlayed(const FileData* file1, const FileData* file2);
|
||||
|
||||
extern const std::vector<FileData::SortType> SortTypes;
|
||||
};
|
252
es-app/src/Gamelist.cpp
Normal file
252
es-app/src/Gamelist.cpp
Normal file
|
@ -0,0 +1,252 @@
|
|||
#include "Gamelist.h"
|
||||
#include "SystemData.h"
|
||||
#include "pugixml/pugixml.hpp"
|
||||
#include <boost/filesystem.hpp>
|
||||
#include "Log.h"
|
||||
#include "Settings.h"
|
||||
#include "Util.h"
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
FileData* findOrCreateFile(SystemData* system, const boost::filesystem::path& path, FileType type)
|
||||
{
|
||||
// first, verify that path is within the system's root folder
|
||||
FileData* root = system->getRootFolder();
|
||||
|
||||
bool contains = false;
|
||||
fs::path relative = removeCommonPath(path, root->getPath(), contains);
|
||||
if(!contains)
|
||||
{
|
||||
LOG(LogError) << "File path \"" << path << "\" is outside system path \"" << system->getStartPath() << "\"";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto path_it = relative.begin();
|
||||
FileData* treeNode = root;
|
||||
bool found = false;
|
||||
while(path_it != relative.end())
|
||||
{
|
||||
const std::vector<FileData*>& children = treeNode->getChildren();
|
||||
found = false;
|
||||
for(auto child_it = children.begin(); child_it != children.end(); child_it++)
|
||||
{
|
||||
if((*child_it)->getPath().filename() == *path_it)
|
||||
{
|
||||
treeNode = *child_it;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// this is the end
|
||||
if(path_it == --relative.end())
|
||||
{
|
||||
if(found)
|
||||
return treeNode;
|
||||
|
||||
if(type == FOLDER)
|
||||
{
|
||||
LOG(LogWarning) << "gameList: folder doesn't already exist, won't create";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
FileData* file = new FileData(type, path, system);
|
||||
treeNode->addChild(file);
|
||||
return file;
|
||||
}
|
||||
|
||||
if(!found)
|
||||
{
|
||||
// don't create folders unless it's leading up to a game
|
||||
// if type is a folder it's gonna be empty, so don't bother
|
||||
if(type == FOLDER)
|
||||
{
|
||||
LOG(LogWarning) << "gameList: folder doesn't already exist, won't create";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// create missing folder
|
||||
FileData* folder = new FileData(FOLDER, treeNode->getPath().stem() / *path_it, system);
|
||||
treeNode->addChild(folder);
|
||||
treeNode = folder;
|
||||
}
|
||||
|
||||
path_it++;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void parseGamelist(SystemData* system)
|
||||
{
|
||||
std::string xmlpath = system->getGamelistPath(false);
|
||||
|
||||
if(!boost::filesystem::exists(xmlpath))
|
||||
return;
|
||||
|
||||
LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"...";
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_file(xmlpath.c_str());
|
||||
|
||||
if(!result)
|
||||
{
|
||||
LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " << result.description();
|
||||
return;
|
||||
}
|
||||
|
||||
pugi::xml_node root = doc.child("gameList");
|
||||
if(!root)
|
||||
{
|
||||
LOG(LogError) << "Could not find <gameList> node in gamelist \"" << xmlpath << "\"!";
|
||||
return;
|
||||
}
|
||||
|
||||
fs::path relativeTo = system->getStartPath();
|
||||
|
||||
const char* tagList[2] = { "game", "folder" };
|
||||
FileType typeList[2] = { GAME, FOLDER };
|
||||
for(int i = 0; i < 2; i++)
|
||||
{
|
||||
const char* tag = tagList[i];
|
||||
FileType type = typeList[i];
|
||||
for(pugi::xml_node fileNode = root.child(tag); fileNode; fileNode = fileNode.next_sibling(tag))
|
||||
{
|
||||
fs::path path = resolvePath(fileNode.child("path").text().get(), relativeTo, false);
|
||||
|
||||
if(!boost::filesystem::exists(path))
|
||||
{
|
||||
LOG(LogWarning) << "File \"" << path << "\" does not exist! Ignoring.";
|
||||
continue;
|
||||
}
|
||||
|
||||
FileData* file = findOrCreateFile(system, path, type);
|
||||
if(!file)
|
||||
{
|
||||
LOG(LogError) << "Error finding/creating FileData for \"" << path << "\", skipping.";
|
||||
continue;
|
||||
}
|
||||
|
||||
//load the metadata
|
||||
std::string defaultName = file->metadata.get("name");
|
||||
file->metadata = MetaDataList::createFromXML(GAME_METADATA, fileNode, relativeTo);
|
||||
|
||||
//make sure name gets set if one didn't exist
|
||||
if(file->metadata.get("name").empty())
|
||||
file->metadata.set("name", defaultName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addFileDataNode(pugi::xml_node& parent, const FileData* file, const char* tag, SystemData* system)
|
||||
{
|
||||
//create game and add to parent node
|
||||
pugi::xml_node newNode = parent.append_child(tag);
|
||||
|
||||
//write metadata
|
||||
file->metadata.appendToXML(newNode, true, system->getStartPath());
|
||||
|
||||
if(newNode.children().begin() == newNode.child("name") //first element is name
|
||||
&& ++newNode.children().begin() == newNode.children().end() //theres only one element
|
||||
&& newNode.child("name").text().get() == file->getCleanName()) //the name is the default
|
||||
{
|
||||
//if the only info is the default name, don't bother with this node
|
||||
//delete it and ultimately do nothing
|
||||
parent.remove_child(newNode);
|
||||
}else{
|
||||
//there's something useful in there so we'll keep the node, add the path
|
||||
|
||||
// try and make the path relative if we can so things still work if we change the rom folder location in the future
|
||||
newNode.prepend_child("path").text().set(makeRelativePath(file->getPath(), system->getStartPath(), false).generic_string().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void updateGamelist(SystemData* system)
|
||||
{
|
||||
//We do this by reading the XML again, adding changes and then writing it back,
|
||||
//because there might be information missing in our systemdata which would then miss in the new XML.
|
||||
//We have the complete information for every game though, so we can simply remove a game
|
||||
//we already have in the system from the XML, and then add it back from its GameData information...
|
||||
|
||||
if(Settings::getInstance()->getBool("IgnoreGamelist"))
|
||||
return;
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_node root;
|
||||
std::string xmlReadPath = system->getGamelistPath(false);
|
||||
|
||||
if(boost::filesystem::exists(xmlReadPath))
|
||||
{
|
||||
//parse an existing file first
|
||||
pugi::xml_parse_result result = doc.load_file(xmlReadPath.c_str());
|
||||
|
||||
if(!result)
|
||||
{
|
||||
LOG(LogError) << "Error parsing XML file \"" << xmlReadPath << "\"!\n " << result.description();
|
||||
return;
|
||||
}
|
||||
|
||||
root = doc.child("gameList");
|
||||
if(!root)
|
||||
{
|
||||
LOG(LogError) << "Could not find <gameList> node in gamelist \"" << xmlReadPath << "\"!";
|
||||
return;
|
||||
}
|
||||
}else{
|
||||
//set up an empty gamelist to append to
|
||||
root = doc.append_child("gameList");
|
||||
}
|
||||
|
||||
|
||||
//now we have all the information from the XML. now iterate through all our games and add information from there
|
||||
FileData* rootFolder = system->getRootFolder();
|
||||
if (rootFolder != nullptr)
|
||||
{
|
||||
//get only files, no folders
|
||||
std::vector<FileData*> files = rootFolder->getFilesRecursive(GAME | FOLDER);
|
||||
//iterate through all files, checking if they're already in the XML
|
||||
std::vector<FileData*>::const_iterator fit = files.cbegin();
|
||||
while(fit != files.cend())
|
||||
{
|
||||
const char* tag = ((*fit)->getType() == GAME) ? "game" : "folder";
|
||||
|
||||
// check if the file already exists in the XML
|
||||
// if it does, remove it before adding
|
||||
for(pugi::xml_node fileNode = root.child(tag); fileNode; fileNode = fileNode.next_sibling(tag))
|
||||
{
|
||||
pugi::xml_node pathNode = fileNode.child("path");
|
||||
if(!pathNode)
|
||||
{
|
||||
LOG(LogError) << "<" << tag << "> node contains no <path> child!";
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::path nodePath = resolvePath(pathNode.text().get(), system->getStartPath(), true);
|
||||
fs::path gamePath((*fit)->getPath());
|
||||
if(nodePath == gamePath || (fs::exists(nodePath) && fs::exists(gamePath) && fs::equivalent(nodePath, gamePath)))
|
||||
{
|
||||
// found it
|
||||
root.remove_child(fileNode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// it was either removed or never existed to begin with; either way, we can add it now
|
||||
addFileDataNode(root, *fit, tag, system);
|
||||
|
||||
++fit;
|
||||
}
|
||||
|
||||
//now write the file
|
||||
|
||||
//make sure the folders leading up to this path exist (or the write will fail)
|
||||
boost::filesystem::path xmlWritePath(system->getGamelistPath(true));
|
||||
boost::filesystem::create_directories(xmlWritePath.parent_path());
|
||||
|
||||
if (!doc.save_file(xmlWritePath.c_str())) {
|
||||
LOG(LogError) << "Error saving gamelist.xml to \"" << xmlWritePath << "\" (for system " << system->getName() << ")!";
|
||||
}
|
||||
}else{
|
||||
LOG(LogError) << "Found no root folder for system \"" << system->getName() << "\"!";
|
||||
}
|
||||
}
|
9
es-app/src/Gamelist.h
Normal file
9
es-app/src/Gamelist.h
Normal file
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
class SystemData;
|
||||
|
||||
// Loads gamelist.xml data into a SystemData.
|
||||
void parseGamelist(SystemData* system);
|
||||
|
||||
// Writes currently loaded metadata for a SystemData to gamelist.xml.
|
||||
void updateGamelist(SystemData* system);
|
30048
es-app/src/MameNameMap.cpp
Normal file
30048
es-app/src/MameNameMap.cpp
Normal file
File diff suppressed because it is too large
Load diff
135
es-app/src/MetaData.cpp
Normal file
135
es-app/src/MetaData.cpp
Normal file
|
@ -0,0 +1,135 @@
|
|||
#include "MetaData.h"
|
||||
#include "components/TextComponent.h"
|
||||
#include "Log.h"
|
||||
#include "Util.h"
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
MetaDataDecl gameDecls[] = {
|
||||
// key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd
|
||||
{"name", MD_STRING, "", false, "name", "enter game name"},
|
||||
{"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"},
|
||||
{"image", MD_IMAGE_PATH, "", false, "image", "enter path to image"},
|
||||
{"thumbnail", MD_IMAGE_PATH, "", false, "thumbnail", "enter path to thumbnail"},
|
||||
{"rating", MD_RATING, "0.000000", false, "rating", "enter rating"},
|
||||
{"releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date"},
|
||||
{"developer", MD_STRING, "unknown", false, "developer", "enter game developer"},
|
||||
{"publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher"},
|
||||
{"genre", MD_STRING, "unknown", false, "genre", "enter game genre"},
|
||||
{"players", MD_INT, "1", false, "players", "enter number of players"},
|
||||
{"playcount", MD_INT, "0", true, "play count", "enter number of times played"},
|
||||
{"lastplayed", MD_TIME, "0", true, "last played", "enter last played date"}
|
||||
};
|
||||
const std::vector<MetaDataDecl> gameMDD(gameDecls, gameDecls + sizeof(gameDecls) / sizeof(gameDecls[0]));
|
||||
|
||||
MetaDataDecl folderDecls[] = {
|
||||
{"name", MD_STRING, "", false},
|
||||
{"desc", MD_MULTILINE_STRING, "", false},
|
||||
{"image", MD_IMAGE_PATH, "", false},
|
||||
{"thumbnail", MD_IMAGE_PATH, "", false},
|
||||
};
|
||||
const std::vector<MetaDataDecl> folderMDD(folderDecls, folderDecls + sizeof(folderDecls) / sizeof(folderDecls[0]));
|
||||
|
||||
const std::vector<MetaDataDecl>& getMDDByType(MetaDataListType type)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case GAME_METADATA:
|
||||
return gameMDD;
|
||||
case FOLDER_METADATA:
|
||||
return folderMDD;
|
||||
}
|
||||
|
||||
LOG(LogError) << "Invalid MDD type";
|
||||
return gameMDD;
|
||||
}
|
||||
|
||||
|
||||
|
||||
MetaDataList::MetaDataList(MetaDataListType type)
|
||||
: mType(type)
|
||||
{
|
||||
const std::vector<MetaDataDecl>& mdd = getMDD();
|
||||
for(auto iter = mdd.begin(); iter != mdd.end(); iter++)
|
||||
set(iter->key, iter->defaultValue);
|
||||
}
|
||||
|
||||
|
||||
MetaDataList MetaDataList::createFromXML(MetaDataListType type, pugi::xml_node node, const fs::path& relativeTo)
|
||||
{
|
||||
MetaDataList mdl(type);
|
||||
|
||||
const std::vector<MetaDataDecl>& mdd = mdl.getMDD();
|
||||
|
||||
for(auto iter = mdd.begin(); iter != mdd.end(); iter++)
|
||||
{
|
||||
pugi::xml_node md = node.child(iter->key.c_str());
|
||||
if(md)
|
||||
{
|
||||
// if it's a path, resolve relative paths
|
||||
std::string value = md.text().get();
|
||||
if(iter->type == MD_IMAGE_PATH)
|
||||
value = resolvePath(value, relativeTo, true).generic_string();
|
||||
|
||||
mdl.set(iter->key, value);
|
||||
}else{
|
||||
mdl.set(iter->key, iter->defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
return mdl;
|
||||
}
|
||||
|
||||
void MetaDataList::appendToXML(pugi::xml_node parent, bool ignoreDefaults, const fs::path& relativeTo) const
|
||||
{
|
||||
const std::vector<MetaDataDecl>& mdd = getMDD();
|
||||
|
||||
for(auto mddIter = mdd.begin(); mddIter != mdd.end(); mddIter++)
|
||||
{
|
||||
auto mapIter = mMap.find(mddIter->key);
|
||||
if(mapIter != mMap.end())
|
||||
{
|
||||
// we have this value!
|
||||
// if it's just the default (and we ignore defaults), don't write it
|
||||
if(ignoreDefaults && mapIter->second == mddIter->defaultValue)
|
||||
continue;
|
||||
|
||||
// try and make paths relative if we can
|
||||
std::string value = mapIter->second;
|
||||
if(mddIter->type == MD_IMAGE_PATH)
|
||||
value = makeRelativePath(value, relativeTo, true).generic_string();
|
||||
|
||||
parent.append_child(mapIter->first.c_str()).text().set(value.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MetaDataList::set(const std::string& key, const std::string& value)
|
||||
{
|
||||
mMap[key] = value;
|
||||
}
|
||||
|
||||
void MetaDataList::setTime(const std::string& key, const boost::posix_time::ptime& time)
|
||||
{
|
||||
mMap[key] = boost::posix_time::to_iso_string(time);
|
||||
}
|
||||
|
||||
const std::string& MetaDataList::get(const std::string& key) const
|
||||
{
|
||||
return mMap.at(key);
|
||||
}
|
||||
|
||||
int MetaDataList::getInt(const std::string& key) const
|
||||
{
|
||||
return atoi(get(key).c_str());
|
||||
}
|
||||
|
||||
float MetaDataList::getFloat(const std::string& key) const
|
||||
{
|
||||
return (float)atof(get(key).c_str());
|
||||
}
|
||||
|
||||
boost::posix_time::ptime MetaDataList::getTime(const std::string& key) const
|
||||
{
|
||||
return string_to_ptime(get(key), "%Y%m%dT%H%M%S%F%q");
|
||||
}
|
65
es-app/src/MetaData.h
Normal file
65
es-app/src/MetaData.h
Normal file
|
@ -0,0 +1,65 @@
|
|||
#pragma once
|
||||
|
||||
#include "pugixml/pugixml.hpp"
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include "GuiComponent.h"
|
||||
#include <boost/date_time.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
enum MetaDataType
|
||||
{
|
||||
//generic types
|
||||
MD_STRING,
|
||||
MD_INT,
|
||||
MD_FLOAT,
|
||||
|
||||
//specialized types
|
||||
MD_MULTILINE_STRING,
|
||||
MD_IMAGE_PATH,
|
||||
MD_RATING,
|
||||
MD_DATE,
|
||||
MD_TIME //used for lastplayed
|
||||
};
|
||||
|
||||
struct MetaDataDecl
|
||||
{
|
||||
std::string key;
|
||||
MetaDataType type;
|
||||
std::string defaultValue;
|
||||
bool isStatistic; //if true, ignore scraper values for this metadata
|
||||
std::string displayName; // displayed as this in editors
|
||||
std::string displayPrompt; // phrase displayed in editors when prompted to enter value (currently only for strings)
|
||||
};
|
||||
|
||||
enum MetaDataListType
|
||||
{
|
||||
GAME_METADATA,
|
||||
FOLDER_METADATA
|
||||
};
|
||||
|
||||
const std::vector<MetaDataDecl>& getMDDByType(MetaDataListType type);
|
||||
|
||||
class MetaDataList
|
||||
{
|
||||
public:
|
||||
static MetaDataList createFromXML(MetaDataListType type, pugi::xml_node node, const boost::filesystem::path& relativeTo);
|
||||
void appendToXML(pugi::xml_node parent, bool ignoreDefaults, const boost::filesystem::path& relativeTo) const;
|
||||
|
||||
MetaDataList(MetaDataListType type);
|
||||
|
||||
void set(const std::string& key, const std::string& value);
|
||||
void setTime(const std::string& key, const boost::posix_time::ptime& time); //times are internally stored as ISO strings (e.g. boost::posix_time::to_iso_string(ptime))
|
||||
|
||||
const std::string& get(const std::string& key) const;
|
||||
int getInt(const std::string& key) const;
|
||||
float getFloat(const std::string& key) const;
|
||||
boost::posix_time::ptime getTime(const std::string& key) const;
|
||||
|
||||
inline MetaDataListType getType() const { return mType; }
|
||||
inline const std::vector<MetaDataDecl>& getMDD() const { return getMDDByType(getType()); }
|
||||
|
||||
private:
|
||||
MetaDataListType mType;
|
||||
std::map<std::string, std::string> mMap;
|
||||
};
|
100
es-app/src/PlatformId.cpp
Normal file
100
es-app/src/PlatformId.cpp
Normal file
|
@ -0,0 +1,100 @@
|
|||
#include "PlatformId.h"
|
||||
#include <string.h>
|
||||
|
||||
extern const char* mameNameToRealName[];
|
||||
|
||||
namespace PlatformIds
|
||||
{
|
||||
const char* PlatformNames[PLATFORM_COUNT + 1] = {
|
||||
"unknown", // nothing set
|
||||
|
||||
"3do",
|
||||
"amiga",
|
||||
"amstradcpc",
|
||||
"apple2",
|
||||
"arcade",
|
||||
"atari800",
|
||||
"atari2600",
|
||||
"atari5200",
|
||||
"atari7800",
|
||||
"atarilynx",
|
||||
"atarist",
|
||||
"atarijaguar",
|
||||
"atarijaguarcd",
|
||||
"atarixe",
|
||||
"colecovision",
|
||||
"c64", // commodore 64
|
||||
"intellivision",
|
||||
"macintosh",
|
||||
"xbox",
|
||||
"xbox360",
|
||||
"neogeo",
|
||||
"ngp", // neo geo pocket
|
||||
"ngpc", // neo geo pocket color
|
||||
"n3ds", // nintendo 3DS
|
||||
"n64", // nintendo 64
|
||||
"nds", // nintendo DS
|
||||
"nes", // nintendo entertainment system
|
||||
"gb", // game boy
|
||||
"gba", // game boy advance
|
||||
"gbc", // game boy color
|
||||
"gc", // gamecube
|
||||
"wii",
|
||||
"wiiu",
|
||||
"pc",
|
||||
"sega32x",
|
||||
"segacd",
|
||||
"dreamcast",
|
||||
"gamegear",
|
||||
"genesis", // sega genesis
|
||||
"mastersystem", // sega master system
|
||||
"megadrive", // sega megadrive
|
||||
"saturn", // sega saturn
|
||||
"psx",
|
||||
"ps2",
|
||||
"ps3",
|
||||
"ps4",
|
||||
"psvita",
|
||||
"psp", // playstation portable
|
||||
"snes", // super nintendo entertainment system
|
||||
"pcengine", // turbografx-16/pcengine
|
||||
"wonderswan",
|
||||
"wonderswancolor",
|
||||
"zxspectrum",
|
||||
|
||||
"ignore", // do not allow scraping for this system
|
||||
"invalid"
|
||||
};
|
||||
|
||||
PlatformId getPlatformId(const char* str)
|
||||
{
|
||||
if(str == NULL)
|
||||
return PLATFORM_UNKNOWN;
|
||||
|
||||
for(unsigned int i = 1; i < PLATFORM_COUNT; i++)
|
||||
{
|
||||
if(strcmp(PlatformNames[i], str) == 0)
|
||||
return (PlatformId)i;
|
||||
}
|
||||
|
||||
return PLATFORM_UNKNOWN;
|
||||
}
|
||||
|
||||
const char* getPlatformName(PlatformId id)
|
||||
{
|
||||
return PlatformNames[id];
|
||||
}
|
||||
|
||||
const char* getCleanMameName(const char* from)
|
||||
{
|
||||
const char** mameNames = mameNameToRealName;
|
||||
|
||||
while(*mameNames != NULL && strcmp(from, *mameNames) != 0)
|
||||
mameNames += 2;
|
||||
|
||||
if(*mameNames)
|
||||
return *(mameNames + 1);
|
||||
|
||||
return from;
|
||||
}
|
||||
}
|
73
es-app/src/PlatformId.h
Normal file
73
es-app/src/PlatformId.h
Normal file
|
@ -0,0 +1,73 @@
|
|||
#pragma once
|
||||
|
||||
#include <map>
|
||||
|
||||
namespace PlatformIds
|
||||
{
|
||||
enum PlatformId : unsigned int
|
||||
{
|
||||
PLATFORM_UNKNOWN = 0,
|
||||
|
||||
THREEDO, // name can't start with a constant
|
||||
AMIGA,
|
||||
AMSTRAD_CPC,
|
||||
APPLE_II,
|
||||
ARCADE,
|
||||
ATARI_800,
|
||||
ATARI_2600,
|
||||
ATARI_5200,
|
||||
ATARI_7800,
|
||||
ATARI_LYNX,
|
||||
ATARI_ST, // Atari ST/STE/Falcon
|
||||
ATARI_JAGUAR,
|
||||
ATARI_JAGUAR_CD,
|
||||
ATARI_XE,
|
||||
COLECOVISION,
|
||||
COMMODORE_64,
|
||||
INTELLIVISION,
|
||||
MAC_OS,
|
||||
XBOX,
|
||||
XBOX_360,
|
||||
NEOGEO,
|
||||
NEOGEO_POCKET,
|
||||
NEOGEO_POCKET_COLOR,
|
||||
NINTENDO_3DS,
|
||||
NINTENDO_64,
|
||||
NINTENDO_DS,
|
||||
NINTENDO_ENTERTAINMENT_SYSTEM,
|
||||
GAME_BOY,
|
||||
GAME_BOY_ADVANCE,
|
||||
GAME_BOY_COLOR,
|
||||
NINTENDO_GAMECUBE,
|
||||
NINTENDO_WII,
|
||||
NINTENDO_WII_U,
|
||||
PC,
|
||||
SEGA_32X,
|
||||
SEGA_CD,
|
||||
SEGA_DREAMCAST,
|
||||
SEGA_GAME_GEAR,
|
||||
SEGA_GENESIS,
|
||||
SEGA_MASTER_SYSTEM,
|
||||
SEGA_MEGA_DRIVE,
|
||||
SEGA_SATURN,
|
||||
PLAYSTATION,
|
||||
PLAYSTATION_2,
|
||||
PLAYSTATION_3,
|
||||
PLAYSTATION_4,
|
||||
PLAYSTATION_VITA,
|
||||
PLAYSTATION_PORTABLE,
|
||||
SUPER_NINTENDO,
|
||||
TURBOGRAFX_16, // (also PC Engine)
|
||||
WONDERSWAN,
|
||||
WONDERSWAN_COLOR,
|
||||
ZX_SPECTRUM,
|
||||
|
||||
PLATFORM_IGNORE, // do not allow scraping for this system
|
||||
PLATFORM_COUNT
|
||||
};
|
||||
|
||||
PlatformId getPlatformId(const char* str);
|
||||
const char* getPlatformName(PlatformId id);
|
||||
|
||||
const char* getCleanMameName(const char* from);
|
||||
}
|
285
es-app/src/ScraperCmdLine.cpp
Normal file
285
es-app/src/ScraperCmdLine.cpp
Normal file
|
@ -0,0 +1,285 @@
|
|||
#include "ScraperCmdLine.h"
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include "SystemData.h"
|
||||
#include "Settings.h"
|
||||
#include <signal.h>
|
||||
#include "Log.h"
|
||||
|
||||
std::ostream& out = std::cout;
|
||||
|
||||
void handle_interrupt_signal(int p)
|
||||
{
|
||||
sleep(50);
|
||||
|
||||
LOG(LogInfo) << "Interrupt received during scrape...";
|
||||
|
||||
SystemData::deleteSystems();
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
int run_scraper_cmdline()
|
||||
{
|
||||
out << "EmulationStation scraper\n";
|
||||
out << "========================\n";
|
||||
out << "\n";
|
||||
|
||||
signal(SIGINT, handle_interrupt_signal);
|
||||
|
||||
//==================================================================================
|
||||
//filter
|
||||
//==================================================================================
|
||||
enum FilterChoice
|
||||
{
|
||||
FILTER_MISSING_IMAGES,
|
||||
FILTER_ALL
|
||||
};
|
||||
|
||||
int filter_choice;
|
||||
do {
|
||||
out << "Select filter for games to be scraped:\n";
|
||||
out << FILTER_MISSING_IMAGES << " - games missing images\n";
|
||||
out << FILTER_ALL << " - all games period, can overwrite existing metadata\n";
|
||||
|
||||
std::cin >> filter_choice;
|
||||
std::cin.ignore(1, '\n'); //skip the unconsumed newline
|
||||
} while(filter_choice < FILTER_MISSING_IMAGES || filter_choice > FILTER_ALL);
|
||||
|
||||
out << "\n";
|
||||
|
||||
//==================================================================================
|
||||
//platforms
|
||||
//==================================================================================
|
||||
|
||||
std::vector<SystemData*> systems;
|
||||
|
||||
out << "You can scrape only specific platforms, or scrape all of them.\n";
|
||||
out << "Would you like to scrape all platforms? (y/n)\n";
|
||||
|
||||
std::string system_choice;
|
||||
std::getline(std::cin, system_choice);
|
||||
|
||||
if(system_choice == "y" || system_choice == "Y")
|
||||
{
|
||||
out << "Will scrape all platforms.\n";
|
||||
for(auto i = SystemData::sSystemVector.begin(); i != SystemData::sSystemVector.end(); i++)
|
||||
{
|
||||
out << " " << (*i)->getName() << " (" << (*i)->getGameCount() << " games)\n";
|
||||
systems.push_back(*i);
|
||||
}
|
||||
|
||||
}else{
|
||||
std::string sys_name;
|
||||
|
||||
out << "Enter the names of the platforms you would like to scrape, one at a time.\n";
|
||||
out << "Type nothing and press enter when you are ready to continue.\n";
|
||||
|
||||
do {
|
||||
for(auto i = SystemData::sSystemVector.begin(); i != SystemData::sSystemVector.end(); i++)
|
||||
{
|
||||
if(std::find(systems.begin(), systems.end(), (*i)) != systems.end())
|
||||
out << " C ";
|
||||
else
|
||||
out << " ";
|
||||
|
||||
out << "\"" << (*i)->getName() << "\" (" << (*i)->getGameCount() << " games)\n";
|
||||
}
|
||||
|
||||
std::getline(std::cin, sys_name);
|
||||
|
||||
if(sys_name.empty())
|
||||
break;
|
||||
|
||||
bool found = false;
|
||||
for(auto i = SystemData::sSystemVector.begin(); i != SystemData::sSystemVector.end(); i++)
|
||||
{
|
||||
if((*i)->getName() == sys_name)
|
||||
{
|
||||
systems.push_back(*i);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!found)
|
||||
out << "System not found.\n";
|
||||
|
||||
} while(true);
|
||||
}
|
||||
|
||||
//==================================================================================
|
||||
//manual mode
|
||||
//==================================================================================
|
||||
|
||||
out << "\n";
|
||||
out << "You can let the scraper try to automatically choose the best result, or\n";
|
||||
out << "you can manually approve each result. This \"manual mode\" is much more accurate.\n";
|
||||
out << "It is highly recommended you use manual mode unless you have a very large collection.\n";
|
||||
out << "Scrape in manual mode? (y/n)\n";
|
||||
|
||||
std::string manual_mode_str;
|
||||
std::getline(std::cin, manual_mode_str);
|
||||
|
||||
bool manual_mode = false;
|
||||
|
||||
if(manual_mode_str == "y" || manual_mode_str == "Y")
|
||||
{
|
||||
manual_mode = true;
|
||||
out << "Scraping in manual mode!\n";
|
||||
}else{
|
||||
out << "Scraping in automatic mode!\n";
|
||||
}
|
||||
|
||||
//==================================================================================
|
||||
//scraping
|
||||
//==================================================================================
|
||||
out << "\n";
|
||||
out << "Alright, let's do this thing!\n";
|
||||
out << "=============================\n";
|
||||
|
||||
/*
|
||||
std::shared_ptr<Scraper> scraper = Settings::getInstance()->getScraper();
|
||||
for(auto sysIt = systems.begin(); sysIt != systems.end(); sysIt++)
|
||||
{
|
||||
std::vector<FileData*> files = (*sysIt)->getRootFolder()->getFilesRecursive(GAME);
|
||||
|
||||
ScraperSearchParams params;
|
||||
params.system = (*sysIt);
|
||||
|
||||
for(auto gameIt = files.begin(); gameIt != files.end(); gameIt++)
|
||||
{
|
||||
params.nameOverride = "";
|
||||
params.game = *gameIt;
|
||||
|
||||
//print original search term
|
||||
out << getCleanFileName(params.game->getPath()) << "...\n";
|
||||
|
||||
//need to take into account filter_choice
|
||||
if(filter_choice == FILTER_MISSING_IMAGES)
|
||||
{
|
||||
if(!params.game->metadata.get("image").empty()) //maybe should also check if the image file exists/is a URL
|
||||
{
|
||||
out << " Skipping, metadata \"image\" entry is not empty.\n";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
//actually get some results
|
||||
do {
|
||||
std::vector<MetaDataList> mdls = scraper->getResults(params);
|
||||
|
||||
//no results
|
||||
if(mdls.size() == 0)
|
||||
{
|
||||
if(manual_mode)
|
||||
{
|
||||
//in manual mode let the user enter a custom search
|
||||
out << " NO RESULTS FOUND! Enter a new name to search for, or nothing to skip.\n";
|
||||
|
||||
std::getline(std::cin, params.nameOverride);
|
||||
|
||||
if(params.nameOverride.empty())
|
||||
{
|
||||
out << " Skipping...\n";
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
|
||||
}else{
|
||||
out << " NO RESULTS FOUND! Skipping...\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//some results
|
||||
if(manual_mode)
|
||||
{
|
||||
//print list of choices
|
||||
for(unsigned int i = 0; i < mdls.size(); i++)
|
||||
{
|
||||
out << " " << i << " - " << mdls.at(i).get("name") << "\n";
|
||||
}
|
||||
|
||||
int choice = -1;
|
||||
std::string choice_str;
|
||||
|
||||
out << "Your choice: ";
|
||||
|
||||
std::getline(std::cin, choice_str);
|
||||
std::stringstream choice_buff(choice_str); //convert to int
|
||||
choice_buff >> choice;
|
||||
|
||||
if(choice >= 0 && choice < (int)mdls.size())
|
||||
{
|
||||
params.game->metadata = mdls.at(choice);
|
||||
break;
|
||||
}else{
|
||||
out << "Invalid choice.\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
}else{
|
||||
//automatic mode
|
||||
//always choose the first choice
|
||||
out << " name -> " << mdls.at(0).get("name") << "\n";
|
||||
params.game->metadata = mdls.at(0);
|
||||
break;
|
||||
}
|
||||
|
||||
} while(true);
|
||||
|
||||
out << "===================\n";
|
||||
}
|
||||
}
|
||||
|
||||
out << "\n\n";
|
||||
out << "Downloading boxart...\n";
|
||||
|
||||
for(auto sysIt = systems.begin(); sysIt != systems.end(); sysIt++)
|
||||
{
|
||||
std::vector<FileData*> files = (*sysIt)->getRootFolder()->getFilesRecursive(GAME);
|
||||
|
||||
for(auto gameIt = files.begin(); gameIt != files.end(); gameIt++)
|
||||
{
|
||||
FileData* game = *gameIt;
|
||||
const std::vector<MetaDataDecl>& mdd = game->metadata.getMDD();
|
||||
for(auto i = mdd.begin(); i != mdd.end(); i++)
|
||||
{
|
||||
std::string key = i->key;
|
||||
std::string url = game->metadata.get(key);
|
||||
|
||||
if(i->type == MD_IMAGE_PATH && HttpReq::isUrl(url))
|
||||
{
|
||||
std::string urlShort = url.substr(0, url.length() > 35 ? 35 : url.length());
|
||||
if(url.length() != urlShort.length()) urlShort += "...";
|
||||
|
||||
out << " " << game->metadata.get("name") << " [from: " << urlShort << "]...\n";
|
||||
|
||||
ScraperSearchParams p;
|
||||
p.game = game;
|
||||
p.system = *sysIt;
|
||||
game->metadata.set(key, downloadImage(url, getSaveAsPath(p, key, url)));
|
||||
if(game->metadata.get(key).empty())
|
||||
{
|
||||
out << " FAILED! Skipping.\n";
|
||||
game->metadata.set(key, url); //result URL to what it was if download failed, retry some other time
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
out << "\n\n";
|
||||
out << "==============================\n";
|
||||
out << "SCRAPE COMPLETE!\n";
|
||||
out << "==============================\n";
|
||||
*/
|
||||
|
||||
out << "\n\n";
|
||||
out << "ACTUALLY THIS IS STILL TODO\n";
|
||||
|
||||
return 0;
|
||||
}
|
3
es-app/src/ScraperCmdLine.h
Normal file
3
es-app/src/ScraperCmdLine.h
Normal file
|
@ -0,0 +1,3 @@
|
|||
#pragma once
|
||||
|
||||
int run_scraper_cmdline();
|
444
es-app/src/SystemData.cpp
Normal file
444
es-app/src/SystemData.cpp
Normal file
|
@ -0,0 +1,444 @@
|
|||
#include "SystemData.h"
|
||||
#include "Gamelist.h"
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <fstream>
|
||||
#include <stdlib.h>
|
||||
#include <SDL_joystick.h>
|
||||
#include "Renderer.h"
|
||||
#include "AudioManager.h"
|
||||
#include "VolumeControl.h"
|
||||
#include "Log.h"
|
||||
#include "InputManager.h"
|
||||
#include <iostream>
|
||||
#include "Settings.h"
|
||||
#include "FileSorts.h"
|
||||
|
||||
std::vector<SystemData*> SystemData::sSystemVector;
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
SystemData::SystemData(const std::string& name, const std::string& fullName, const std::string& startPath, const std::vector<std::string>& extensions,
|
||||
const std::string& command, const std::vector<PlatformIds::PlatformId>& platformIds, const std::string& themeFolder)
|
||||
{
|
||||
mName = name;
|
||||
mFullName = fullName;
|
||||
mStartPath = startPath;
|
||||
|
||||
//expand home symbol if the startpath contains ~
|
||||
if(mStartPath[0] == '~')
|
||||
{
|
||||
mStartPath.erase(0, 1);
|
||||
mStartPath.insert(0, getHomePath());
|
||||
}
|
||||
|
||||
mSearchExtensions = extensions;
|
||||
mLaunchCommand = command;
|
||||
mPlatformIds = platformIds;
|
||||
mThemeFolder = themeFolder;
|
||||
|
||||
mRootFolder = new FileData(FOLDER, mStartPath, this);
|
||||
mRootFolder->metadata.set("name", mFullName);
|
||||
|
||||
if(!Settings::getInstance()->getBool("ParseGamelistOnly"))
|
||||
populateFolder(mRootFolder);
|
||||
|
||||
if(!Settings::getInstance()->getBool("IgnoreGamelist"))
|
||||
parseGamelist(this);
|
||||
|
||||
mRootFolder->sort(FileSorts::SortTypes.at(0));
|
||||
|
||||
loadTheme();
|
||||
}
|
||||
|
||||
SystemData::~SystemData()
|
||||
{
|
||||
//save changed game data back to xml
|
||||
if(!Settings::getInstance()->getBool("IgnoreGamelist"))
|
||||
{
|
||||
updateGamelist(this);
|
||||
}
|
||||
|
||||
delete mRootFolder;
|
||||
}
|
||||
|
||||
std::string strreplace(std::string& str, std::string replace, std::string with)
|
||||
{
|
||||
size_t pos = str.find(replace);
|
||||
|
||||
if(pos != std::string::npos)
|
||||
return str.replace(pos, replace.length(), with.c_str(), with.length());
|
||||
else
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string escapePath(const boost::filesystem::path& path)
|
||||
{
|
||||
// a quick and dirty way to insert a backslash before most characters that would mess up a bash path;
|
||||
// someone with regex knowledge should make this into a one-liner
|
||||
std::string pathStr = path.generic_string();
|
||||
|
||||
const char* invalidChars = " '\"\\!$^&*(){}[]?;<>";
|
||||
for(unsigned int i = 0; i < pathStr.length(); i++)
|
||||
{
|
||||
char c;
|
||||
unsigned int charNum = 0;
|
||||
do {
|
||||
c = invalidChars[charNum];
|
||||
if(pathStr[i] == c)
|
||||
{
|
||||
pathStr.insert(i, "\\");
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
charNum++;
|
||||
} while(c != '\0');
|
||||
}
|
||||
|
||||
return pathStr;
|
||||
}
|
||||
|
||||
void SystemData::launchGame(Window* window, FileData* game)
|
||||
{
|
||||
LOG(LogInfo) << "Attempting to launch game...";
|
||||
|
||||
AudioManager::getInstance()->deinit();
|
||||
VolumeControl::getInstance()->deinit();
|
||||
window->deinit();
|
||||
|
||||
std::string command = mLaunchCommand;
|
||||
|
||||
const std::string rom = escapePath(game->getPath());
|
||||
const std::string basename = game->getPath().stem().string();
|
||||
const std::string rom_raw = game->getPath().string();
|
||||
|
||||
command = strreplace(command, "%ROM%", rom);
|
||||
command = strreplace(command, "%BASENAME%", basename);
|
||||
command = strreplace(command, "%ROM_RAW%", rom_raw);
|
||||
|
||||
LOG(LogInfo) << " " << command;
|
||||
std::cout << "==============================================\n";
|
||||
int exitCode = system(command.c_str());
|
||||
std::cout << "==============================================\n";
|
||||
|
||||
if(exitCode != 0)
|
||||
{
|
||||
LOG(LogWarning) << "...launch terminated with nonzero exit code " << exitCode << "!";
|
||||
}
|
||||
|
||||
window->init();
|
||||
VolumeControl::getInstance()->init();
|
||||
AudioManager::getInstance()->init();
|
||||
window->normalizeNextUpdate();
|
||||
|
||||
//update number of times the game has been launched
|
||||
int timesPlayed = game->metadata.getInt("playcount") + 1;
|
||||
game->metadata.set("playcount", std::to_string(static_cast<long long>(timesPlayed)));
|
||||
|
||||
//update last played time
|
||||
boost::posix_time::ptime time = boost::posix_time::second_clock::universal_time();
|
||||
game->metadata.setTime("lastplayed", time);
|
||||
}
|
||||
|
||||
void SystemData::populateFolder(FileData* folder)
|
||||
{
|
||||
const fs::path& folderPath = folder->getPath();
|
||||
if(!fs::is_directory(folderPath))
|
||||
{
|
||||
LOG(LogWarning) << "Error - folder with path \"" << folderPath << "\" is not a directory!";
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string folderStr = folderPath.generic_string();
|
||||
|
||||
//make sure that this isn't a symlink to a thing we already have
|
||||
if(fs::is_symlink(folderPath))
|
||||
{
|
||||
//if this symlink resolves to somewhere that's at the beginning of our path, it's gonna recurse
|
||||
if(folderStr.find(fs::canonical(folderPath).generic_string()) == 0)
|
||||
{
|
||||
LOG(LogWarning) << "Skipping infinitely recursive symlink \"" << folderPath << "\"";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fs::path filePath;
|
||||
std::string extension;
|
||||
bool isGame;
|
||||
for(fs::directory_iterator end, dir(folderPath); dir != end; ++dir)
|
||||
{
|
||||
filePath = (*dir).path();
|
||||
|
||||
if(filePath.stem().empty())
|
||||
continue;
|
||||
|
||||
//this is a little complicated because we allow a list of extensions to be defined (delimited with a space)
|
||||
//we first get the extension of the file itself:
|
||||
extension = filePath.extension().string();
|
||||
|
||||
//fyi, folders *can* also match the extension and be added as games - this is mostly just to support higan
|
||||
//see issue #75: https://github.com/Aloshi/EmulationStation/issues/75
|
||||
|
||||
isGame = false;
|
||||
if(std::find(mSearchExtensions.begin(), mSearchExtensions.end(), extension) != mSearchExtensions.end())
|
||||
{
|
||||
FileData* newGame = new FileData(GAME, filePath.generic_string(), this);
|
||||
folder->addChild(newGame);
|
||||
isGame = true;
|
||||
}
|
||||
|
||||
//add directories that also do not match an extension as folders
|
||||
if(!isGame && fs::is_directory(filePath))
|
||||
{
|
||||
FileData* newFolder = new FileData(FOLDER, filePath.generic_string(), this);
|
||||
populateFolder(newFolder);
|
||||
|
||||
//ignore folders that do not contain games
|
||||
if(newFolder->getChildren().size() == 0)
|
||||
delete newFolder;
|
||||
else
|
||||
folder->addChild(newFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> readList(const std::string& str, const char* delims = " \t\r\n,")
|
||||
{
|
||||
std::vector<std::string> ret;
|
||||
|
||||
size_t prevOff = str.find_first_not_of(delims, 0);
|
||||
size_t off = str.find_first_of(delims, prevOff);
|
||||
while(off != std::string::npos || prevOff != std::string::npos)
|
||||
{
|
||||
ret.push_back(str.substr(prevOff, off - prevOff));
|
||||
|
||||
prevOff = str.find_first_not_of(delims, off);
|
||||
off = str.find_first_of(delims, prevOff);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
//creates systems from information located in a config file
|
||||
bool SystemData::loadConfig()
|
||||
{
|
||||
deleteSystems();
|
||||
|
||||
std::string path = getConfigPath(false);
|
||||
|
||||
LOG(LogInfo) << "Loading system config file " << path << "...";
|
||||
|
||||
if(!fs::exists(path))
|
||||
{
|
||||
LOG(LogError) << "es_systems.cfg file does not exist!";
|
||||
writeExampleConfig(getConfigPath(true));
|
||||
return false;
|
||||
}
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result res = doc.load_file(path.c_str());
|
||||
|
||||
if(!res)
|
||||
{
|
||||
LOG(LogError) << "Could not parse es_systems.cfg file!";
|
||||
LOG(LogError) << res.description();
|
||||
return false;
|
||||
}
|
||||
|
||||
//actually read the file
|
||||
pugi::xml_node systemList = doc.child("systemList");
|
||||
|
||||
if(!systemList)
|
||||
{
|
||||
LOG(LogError) << "es_systems.cfg is missing the <systemList> tag!";
|
||||
return false;
|
||||
}
|
||||
|
||||
for(pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system"))
|
||||
{
|
||||
std::string name, fullname, path, cmd, themeFolder;
|
||||
PlatformIds::PlatformId platformId = PlatformIds::PLATFORM_UNKNOWN;
|
||||
|
||||
name = system.child("name").text().get();
|
||||
fullname = system.child("fullname").text().get();
|
||||
path = system.child("path").text().get();
|
||||
|
||||
// convert extensions list from a string into a vector of strings
|
||||
std::vector<std::string> extensions = readList(system.child("extension").text().get());
|
||||
|
||||
cmd = system.child("command").text().get();
|
||||
|
||||
// platform id list
|
||||
const char* platformList = system.child("platform").text().get();
|
||||
std::vector<std::string> platformStrs = readList(platformList);
|
||||
std::vector<PlatformIds::PlatformId> platformIds;
|
||||
for(auto it = platformStrs.begin(); it != platformStrs.end(); it++)
|
||||
{
|
||||
const char* str = it->c_str();
|
||||
PlatformIds::PlatformId platformId = PlatformIds::getPlatformId(str);
|
||||
|
||||
if(platformId == PlatformIds::PLATFORM_IGNORE)
|
||||
{
|
||||
// when platform is ignore, do not allow other platforms
|
||||
platformIds.clear();
|
||||
platformIds.push_back(platformId);
|
||||
break;
|
||||
}
|
||||
|
||||
// if there appears to be an actual platform ID supplied but it didn't match the list, warn
|
||||
if(str != NULL && str[0] != '\0' && platformId == PlatformIds::PLATFORM_UNKNOWN)
|
||||
LOG(LogWarning) << " Unknown platform for system \"" << name << "\" (platform \"" << str << "\" from list \"" << platformList << "\")";
|
||||
else if(platformId != PlatformIds::PLATFORM_UNKNOWN)
|
||||
platformIds.push_back(platformId);
|
||||
}
|
||||
|
||||
// theme folder
|
||||
themeFolder = system.child("theme").text().as_string(name.c_str());
|
||||
|
||||
//validate
|
||||
if(name.empty() || path.empty() || extensions.empty() || cmd.empty())
|
||||
{
|
||||
LOG(LogError) << "System \"" << name << "\" is missing name, path, extension, or command!";
|
||||
continue;
|
||||
}
|
||||
|
||||
//convert path to generic directory seperators
|
||||
boost::filesystem::path genericPath(path);
|
||||
path = genericPath.generic_string();
|
||||
|
||||
SystemData* newSys = new SystemData(name, fullname, path, extensions, cmd, platformIds, themeFolder);
|
||||
if(newSys->getRootFolder()->getChildren().size() == 0)
|
||||
{
|
||||
LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it.";
|
||||
delete newSys;
|
||||
}else{
|
||||
sSystemVector.push_back(newSys);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SystemData::writeExampleConfig(const std::string& path)
|
||||
{
|
||||
std::ofstream file(path.c_str());
|
||||
|
||||
file << "<!-- This is the EmulationStation Systems configuration file.\n"
|
||||
"All systems must be contained within the <systemList> tag.-->\n"
|
||||
"\n"
|
||||
"<systemList>\n"
|
||||
" <!-- Here's an example system to get you started. -->\n"
|
||||
" <system>\n"
|
||||
"\n"
|
||||
" <!-- A short name, used internally. Traditionally lower-case. -->\n"
|
||||
" <name>nes</name>\n"
|
||||
"\n"
|
||||
" <!-- A \"pretty\" name, displayed in menus and such. -->\n"
|
||||
" <fullname>Nintendo Entertainment System</fullname>\n"
|
||||
"\n"
|
||||
" <!-- The path to start searching for ROMs in. '~' will be expanded to $HOME on Linux or %HOMEPATH% on Windows. -->\n"
|
||||
" <path>~/roms/nes</path>\n"
|
||||
"\n"
|
||||
" <!-- A list of extensions to search for, delimited by any of the whitespace characters (\", \\r\\n\\t\").\n"
|
||||
" You MUST include the period at the start of the extension! It's also case sensitive. -->\n"
|
||||
" <extension>.nes .NES</extension>\n"
|
||||
"\n"
|
||||
" <!-- The shell command executed when a game is selected. A few special tags are replaced if found in a command:\n"
|
||||
" %ROM% is replaced by a bash-special-character-escaped absolute path to the ROM.\n"
|
||||
" %BASENAME% is replaced by the \"base\" name of the ROM. For example, \"/foo/bar.rom\" would have a basename of \"bar\". Useful for MAME.\n"
|
||||
" %ROM_RAW% is the raw, unescaped path to the ROM. -->\n"
|
||||
" <command>retroarch -L ~/cores/libretro-fceumm.so %ROM%</command>\n"
|
||||
"\n"
|
||||
" <!-- The platform to use when scraping. You can see the full list of accepted platforms in src/PlatformIds.cpp.\n"
|
||||
" It's case sensitive, but everything is lowercase. This tag is optional.\n"
|
||||
" You can use multiple platforms too, delimited with any of the whitespace characters (\", \\r\\n\\t\"), eg: \"genesis, megadrive\" -->\n"
|
||||
" <platform>nes</platform>\n"
|
||||
"\n"
|
||||
" <!-- The theme to load from the current theme set. See THEMES.md for more information.\n"
|
||||
" This tag is optional. If not set, it will default to the value of <name>. -->\n"
|
||||
" <theme>nes</theme>\n"
|
||||
" </system>\n"
|
||||
"</systemList>\n";
|
||||
|
||||
file.close();
|
||||
|
||||
LOG(LogError) << "Example config written! Go read it at \"" << path << "\"!";
|
||||
}
|
||||
|
||||
void SystemData::deleteSystems()
|
||||
{
|
||||
for(unsigned int i = 0; i < sSystemVector.size(); i++)
|
||||
{
|
||||
delete sSystemVector.at(i);
|
||||
}
|
||||
sSystemVector.clear();
|
||||
}
|
||||
|
||||
std::string SystemData::getConfigPath(bool forWrite)
|
||||
{
|
||||
fs::path path = getHomePath() + "/.emulationstation/es_systems.cfg";
|
||||
if(forWrite || fs::exists(path))
|
||||
return path.generic_string();
|
||||
|
||||
return "/etc/emulationstation/es_systems.cfg";
|
||||
}
|
||||
|
||||
std::string SystemData::getGamelistPath(bool forWrite) const
|
||||
{
|
||||
fs::path filePath;
|
||||
|
||||
filePath = mRootFolder->getPath() / "gamelist.xml";
|
||||
if(fs::exists(filePath))
|
||||
return filePath.generic_string();
|
||||
|
||||
filePath = getHomePath() + "/.emulationstation/gamelists/" + mName + "/gamelist.xml";
|
||||
if(forWrite) // make sure the directory exists if we're going to write to it, or crashes will happen
|
||||
fs::create_directories(filePath.parent_path());
|
||||
if(forWrite || fs::exists(filePath))
|
||||
return filePath.generic_string();
|
||||
|
||||
return "/etc/emulationstation/gamelists/" + mName + "/gamelist.xml";
|
||||
}
|
||||
|
||||
std::string SystemData::getThemePath() const
|
||||
{
|
||||
// where we check for themes, in order:
|
||||
// 1. [SYSTEM_PATH]/theme.xml
|
||||
// 2. currently selected theme set
|
||||
|
||||
// first, check game folder
|
||||
fs::path localThemePath = mRootFolder->getPath() / "theme.xml";
|
||||
if(fs::exists(localThemePath))
|
||||
return localThemePath.generic_string();
|
||||
|
||||
// not in game folder, try theme sets
|
||||
return ThemeData::getThemeFromCurrentSet(mThemeFolder).generic_string();
|
||||
}
|
||||
|
||||
bool SystemData::hasGamelist() const
|
||||
{
|
||||
return (fs::exists(getGamelistPath(false)));
|
||||
}
|
||||
|
||||
unsigned int SystemData::getGameCount() const
|
||||
{
|
||||
return mRootFolder->getFilesRecursive(GAME).size();
|
||||
}
|
||||
|
||||
void SystemData::loadTheme()
|
||||
{
|
||||
mTheme = std::make_shared<ThemeData>();
|
||||
|
||||
std::string path = getThemePath();
|
||||
|
||||
if(!fs::exists(path)) // no theme available for this platform
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
mTheme->loadFile(path);
|
||||
} catch(ThemeException& e)
|
||||
{
|
||||
LOG(LogError) << e.what();
|
||||
mTheme = std::make_shared<ThemeData>(); // reset to empty
|
||||
}
|
||||
}
|
80
es-app/src/SystemData.h
Normal file
80
es-app/src/SystemData.h
Normal file
|
@ -0,0 +1,80 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include "FileData.h"
|
||||
#include "Window.h"
|
||||
#include "MetaData.h"
|
||||
#include "PlatformId.h"
|
||||
#include "ThemeData.h"
|
||||
|
||||
class SystemData
|
||||
{
|
||||
public:
|
||||
SystemData(const std::string& name, const std::string& fullName, const std::string& startPath, const std::vector<std::string>& extensions,
|
||||
const std::string& command, const std::vector<PlatformIds::PlatformId>& platformIds, const std::string& themeFolder);
|
||||
~SystemData();
|
||||
|
||||
inline FileData* getRootFolder() const { return mRootFolder; };
|
||||
inline const std::string& getName() const { return mName; }
|
||||
inline const std::string& getFullName() const { return mFullName; }
|
||||
inline const std::string& getStartPath() const { return mStartPath; }
|
||||
inline const std::vector<std::string>& getExtensions() const { return mSearchExtensions; }
|
||||
inline const std::string& getThemeFolder() const { return mThemeFolder; }
|
||||
|
||||
inline const std::vector<PlatformIds::PlatformId>& getPlatformIds() const { return mPlatformIds; }
|
||||
inline bool hasPlatformId(PlatformIds::PlatformId id) { return std::find(mPlatformIds.begin(), mPlatformIds.end(), id) != mPlatformIds.end(); }
|
||||
|
||||
inline const std::shared_ptr<ThemeData>& getTheme() const { return mTheme; }
|
||||
|
||||
std::string getGamelistPath(bool forWrite) const;
|
||||
bool hasGamelist() const;
|
||||
std::string getThemePath() const;
|
||||
|
||||
unsigned int getGameCount() const;
|
||||
|
||||
void launchGame(Window* window, FileData* game);
|
||||
|
||||
static void deleteSystems();
|
||||
static bool loadConfig(); //Load the system config file at getConfigPath(). Returns true if no errors were encountered. An example will be written if the file doesn't exist.
|
||||
static void writeExampleConfig(const std::string& path);
|
||||
static std::string getConfigPath(bool forWrite); // if forWrite, will only return ~/.emulationstation/es_systems.cfg, never /etc/emulationstation/es_systems.cfg
|
||||
|
||||
static std::vector<SystemData*> sSystemVector;
|
||||
|
||||
inline std::vector<SystemData*>::const_iterator getIterator() const { return std::find(sSystemVector.begin(), sSystemVector.end(), this); };
|
||||
inline std::vector<SystemData*>::const_reverse_iterator getRevIterator() const { return std::find(sSystemVector.rbegin(), sSystemVector.rend(), this); };
|
||||
|
||||
inline SystemData* getNext() const
|
||||
{
|
||||
auto it = getIterator();
|
||||
it++;
|
||||
if(it == sSystemVector.end()) it = sSystemVector.begin();
|
||||
return *it;
|
||||
}
|
||||
|
||||
inline SystemData* getPrev() const
|
||||
{
|
||||
auto it = getRevIterator();
|
||||
it++;
|
||||
if(it == sSystemVector.rend()) it = sSystemVector.rbegin();
|
||||
return *it;
|
||||
}
|
||||
|
||||
// Load or re-load theme.
|
||||
void loadTheme();
|
||||
|
||||
private:
|
||||
std::string mName;
|
||||
std::string mFullName;
|
||||
std::string mStartPath;
|
||||
std::vector<std::string> mSearchExtensions;
|
||||
std::string mLaunchCommand;
|
||||
std::vector<PlatformIds::PlatformId> mPlatformIds;
|
||||
std::string mThemeFolder;
|
||||
std::shared_ptr<ThemeData> mTheme;
|
||||
|
||||
void populateFolder(FileData* folder);
|
||||
|
||||
FileData* mRootFolder;
|
||||
};
|
388
es-app/src/VolumeControl.cpp
Normal file
388
es-app/src/VolumeControl.cpp
Normal file
|
@ -0,0 +1,388 @@
|
|||
#include "VolumeControl.h"
|
||||
|
||||
#include "Log.h"
|
||||
|
||||
|
||||
#if defined(__linux__)
|
||||
#ifdef _RPI_
|
||||
const char * VolumeControl::mixerName = "PCM";
|
||||
#else
|
||||
const char * VolumeControl::mixerName = "Master";
|
||||
#endif
|
||||
const char * VolumeControl::mixerCard = "default";
|
||||
#endif
|
||||
|
||||
std::weak_ptr<VolumeControl> VolumeControl::sInstance;
|
||||
|
||||
|
||||
VolumeControl::VolumeControl()
|
||||
: originalVolume(0), internalVolume(0)
|
||||
#if defined (__APPLE__)
|
||||
#error TODO: Not implemented for MacOS yet!!!
|
||||
#elif defined(__linux__)
|
||||
, mixerIndex(0), mixerHandle(nullptr), mixerElem(nullptr), mixerSelemId(nullptr)
|
||||
#elif defined(WIN32) || defined(_WIN32)
|
||||
, mixerHandle(nullptr), endpointVolume(nullptr)
|
||||
#endif
|
||||
{
|
||||
init();
|
||||
|
||||
//get original volume levels for system
|
||||
originalVolume = getVolume();
|
||||
}
|
||||
|
||||
VolumeControl::VolumeControl(const VolumeControl & right)
|
||||
{
|
||||
sInstance = right.sInstance;
|
||||
}
|
||||
|
||||
VolumeControl & VolumeControl::operator=(const VolumeControl & right)
|
||||
{
|
||||
if (this != &right) {
|
||||
sInstance = right.sInstance;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
VolumeControl::~VolumeControl()
|
||||
{
|
||||
//set original volume levels for system
|
||||
setVolume(originalVolume);
|
||||
|
||||
deinit();
|
||||
}
|
||||
|
||||
std::shared_ptr<VolumeControl> & VolumeControl::getInstance()
|
||||
{
|
||||
//check if an VolumeControl instance is already created, if not create one
|
||||
static std::shared_ptr<VolumeControl> sharedInstance = sInstance.lock();
|
||||
if (sharedInstance == nullptr) {
|
||||
sharedInstance.reset(new VolumeControl);
|
||||
sInstance = sharedInstance;
|
||||
}
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
void VolumeControl::init()
|
||||
{
|
||||
//initialize audio mixer interface
|
||||
#if defined (__APPLE__)
|
||||
#error TODO: Not implemented for MacOS yet!!!
|
||||
#elif defined(__linux__)
|
||||
//try to open mixer device
|
||||
if (mixerHandle == nullptr)
|
||||
{
|
||||
snd_mixer_selem_id_alloca(&mixerSelemId);
|
||||
//sets simple-mixer index and name
|
||||
snd_mixer_selem_id_set_index(mixerSelemId, mixerIndex);
|
||||
snd_mixer_selem_id_set_name(mixerSelemId, mixerName);
|
||||
//open mixer
|
||||
if (snd_mixer_open(&mixerHandle, 0) >= 0)
|
||||
{
|
||||
LOG(LogDebug) << "VolumeControl::init() - Opened ALSA mixer";
|
||||
//ok. attach to defualt card
|
||||
if (snd_mixer_attach(mixerHandle, mixerCard) >= 0)
|
||||
{
|
||||
LOG(LogDebug) << "VolumeControl::init() - Attached to default card";
|
||||
//ok. register simple element class
|
||||
if (snd_mixer_selem_register(mixerHandle, NULL, NULL) >= 0)
|
||||
{
|
||||
LOG(LogDebug) << "VolumeControl::init() - Registered simple element class";
|
||||
//ok. load registered elements
|
||||
if (snd_mixer_load(mixerHandle) >= 0)
|
||||
{
|
||||
LOG(LogDebug) << "VolumeControl::init() - Loaded mixer elements";
|
||||
//ok. find elements now
|
||||
mixerElem = snd_mixer_find_selem(mixerHandle, mixerSelemId);
|
||||
if (mixerElem != nullptr)
|
||||
{
|
||||
//wohoo. good to go...
|
||||
LOG(LogDebug) << "VolumeControl::init() - Mixer initialized";
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::init() - Failed to find mixer elements!";
|
||||
snd_mixer_close(mixerHandle);
|
||||
mixerHandle = nullptr;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::init() - Failed to load mixer elements!";
|
||||
snd_mixer_close(mixerHandle);
|
||||
mixerHandle = nullptr;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::init() - Failed to register simple element class!";
|
||||
snd_mixer_close(mixerHandle);
|
||||
mixerHandle = nullptr;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::init() - Failed to attach to default card!";
|
||||
snd_mixer_close(mixerHandle);
|
||||
mixerHandle = nullptr;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::init() - Failed to open ALSA mixer!";
|
||||
}
|
||||
}
|
||||
#elif defined(WIN32) || defined(_WIN32)
|
||||
//get windows version information
|
||||
OSVERSIONINFOEXA osVer = {sizeof(OSVERSIONINFO)};
|
||||
::GetVersionExA(reinterpret_cast<LPOSVERSIONINFOA>(&osVer));
|
||||
//check windows version
|
||||
if(osVer.dwMajorVersion < 6)
|
||||
{
|
||||
//Windows older than Vista. use mixer API. open default mixer
|
||||
if (mixerHandle == nullptr)
|
||||
{
|
||||
if (mixerOpen(&mixerHandle, 0, NULL, 0, 0) == MMSYSERR_NOERROR)
|
||||
{
|
||||
//retrieve info on the volume slider control for the "Speaker Out" line
|
||||
MIXERLINECONTROLS mixerLineControls;
|
||||
mixerLineControls.cbStruct = sizeof(MIXERLINECONTROLS);
|
||||
mixerLineControls.dwLineID = 0xFFFF0000; //Id of "Speaker Out" line
|
||||
mixerLineControls.cControls = 1;
|
||||
//mixerLineControls.dwControlID = 0x00000000; //Id of "Speaker Out" line's volume slider
|
||||
mixerLineControls.dwControlType = MIXERCONTROL_CONTROLTYPE_VOLUME; //Get volume control
|
||||
mixerLineControls.pamxctrl = &mixerControl;
|
||||
mixerLineControls.cbmxctrl = sizeof(MIXERCONTROL);
|
||||
if (mixerGetLineControls((HMIXEROBJ)mixerHandle, &mixerLineControls, MIXER_GETLINECONTROLSF_ONEBYTYPE) != MMSYSERR_NOERROR)
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::getVolume() - Failed to get mixer volume control!";
|
||||
mixerClose(mixerHandle);
|
||||
mixerHandle = nullptr;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::init() - Failed to open mixer!";
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Windows Vista or above. use EndpointVolume API. get device enumerator
|
||||
if (endpointVolume == nullptr)
|
||||
{
|
||||
CoInitialize(nullptr);
|
||||
IMMDeviceEnumerator * deviceEnumerator = nullptr;
|
||||
CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (LPVOID *)&deviceEnumerator);
|
||||
if (deviceEnumerator != nullptr)
|
||||
{
|
||||
//get default endpoint
|
||||
IMMDevice * defaultDevice = nullptr;
|
||||
deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &defaultDevice);
|
||||
if (defaultDevice != nullptr)
|
||||
{
|
||||
//retrieve endpoint volume
|
||||
defaultDevice->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, nullptr, (LPVOID *)&endpointVolume);
|
||||
if (endpointVolume == nullptr)
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::init() - Failed to get default audio endpoint volume!";
|
||||
}
|
||||
//release default device. we don't need it anymore
|
||||
defaultDevice->Release();
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::init() - Failed to get default audio endpoint!";
|
||||
}
|
||||
//release device enumerator. we don't need it anymore
|
||||
deviceEnumerator->Release();
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::init() - Failed to get audio endpoint enumerator!";
|
||||
CoUninitialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void VolumeControl::deinit()
|
||||
{
|
||||
//deinitialize audio mixer interface
|
||||
#if defined (__APPLE__)
|
||||
#error TODO: Not implemented for MacOS yet!!!
|
||||
#elif defined(__linux__)
|
||||
if (mixerHandle != nullptr) {
|
||||
snd_mixer_detach(mixerHandle, mixerCard);
|
||||
snd_mixer_free(mixerHandle);
|
||||
snd_mixer_close(mixerHandle);
|
||||
mixerHandle = nullptr;
|
||||
mixerElem = nullptr;
|
||||
}
|
||||
#elif defined(WIN32) || defined(_WIN32)
|
||||
if (mixerHandle != nullptr) {
|
||||
mixerClose(mixerHandle);
|
||||
mixerHandle = nullptr;
|
||||
}
|
||||
else if (endpointVolume != nullptr) {
|
||||
endpointVolume->Release();
|
||||
endpointVolume = nullptr;
|
||||
CoUninitialize();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
int VolumeControl::getVolume() const
|
||||
{
|
||||
int volume = 0;
|
||||
|
||||
#if defined (__APPLE__)
|
||||
#error TODO: Not implemented for MacOS yet!!!
|
||||
#elif defined(__linux__)
|
||||
if (mixerElem != nullptr)
|
||||
{
|
||||
//get volume range
|
||||
long minVolume;
|
||||
long maxVolume;
|
||||
if (snd_mixer_selem_get_playback_volume_range(mixerElem, &minVolume, &maxVolume) == 0)
|
||||
{
|
||||
//ok. now get volume
|
||||
long rawVolume;
|
||||
if (snd_mixer_selem_get_playback_volume(mixerElem, SND_MIXER_SCHN_MONO, &rawVolume) == 0)
|
||||
{
|
||||
//worked. bring into range 0-100
|
||||
rawVolume -= minVolume;
|
||||
if (rawVolume > 0)
|
||||
{
|
||||
volume = (rawVolume * 100) / (maxVolume - minVolume);
|
||||
}
|
||||
//else volume = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::getVolume() - Failed to get mixer volume!";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::getVolume() - Failed to get volume range!";
|
||||
}
|
||||
}
|
||||
#elif defined(WIN32) || defined(_WIN32)
|
||||
if (mixerHandle != nullptr)
|
||||
{
|
||||
//Windows older than Vista. use mixer API. get volume from line control
|
||||
MIXERCONTROLDETAILS_UNSIGNED value;
|
||||
MIXERCONTROLDETAILS mixerControlDetails;
|
||||
mixerControlDetails.cbStruct = sizeof(MIXERCONTROLDETAILS);
|
||||
mixerControlDetails.dwControlID = mixerControl.dwControlID;
|
||||
mixerControlDetails.cChannels = 1; //always 1 for a MIXERCONTROL_CONTROLF_UNIFORM control
|
||||
mixerControlDetails.cMultipleItems = 0; //always 0 except for a MIXERCONTROL_CONTROLF_MULTIPLE control
|
||||
mixerControlDetails.paDetails = &value;
|
||||
mixerControlDetails.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED);
|
||||
if (mixerGetControlDetails((HMIXEROBJ)mixerHandle, &mixerControlDetails, MIXER_GETCONTROLDETAILSF_VALUE) == MMSYSERR_NOERROR)
|
||||
{
|
||||
volume = (uint8_t)((value.dwValue * 100) / 65535);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::getVolume() - Failed to get mixer volume!";
|
||||
}
|
||||
}
|
||||
else if (endpointVolume != nullptr)
|
||||
{
|
||||
//Windows Vista or above. use EndpointVolume API
|
||||
float floatVolume = 0.0f; //0-1
|
||||
if (endpointVolume->GetMasterVolumeLevelScalar(&floatVolume) == S_OK)
|
||||
{
|
||||
volume = (uint8_t)(floatVolume * 100.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::getVolume() - Failed to get master volume!";
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
//clamp to 0-100 range
|
||||
if (volume < 0)
|
||||
{
|
||||
volume = 0;
|
||||
}
|
||||
if (volume > 100)
|
||||
{
|
||||
volume = 100;
|
||||
}
|
||||
return volume;
|
||||
}
|
||||
|
||||
void VolumeControl::setVolume(int volume)
|
||||
{
|
||||
//clamp to 0-100 range
|
||||
if (volume < 0)
|
||||
{
|
||||
volume = 0;
|
||||
}
|
||||
if (volume > 100)
|
||||
{
|
||||
volume = 100;
|
||||
}
|
||||
//store values in internal variables
|
||||
internalVolume = volume;
|
||||
#if defined (__APPLE__)
|
||||
#error TODO: Not implemented for MacOS yet!!!
|
||||
#elif defined(__linux__)
|
||||
if (mixerElem != nullptr)
|
||||
{
|
||||
//get volume range
|
||||
long minVolume;
|
||||
long maxVolume;
|
||||
if (snd_mixer_selem_get_playback_volume_range(mixerElem, &minVolume, &maxVolume) == 0)
|
||||
{
|
||||
//ok. bring into minVolume-maxVolume range and set
|
||||
long rawVolume = (volume * (maxVolume - minVolume) / 100) + minVolume;
|
||||
if (snd_mixer_selem_set_playback_volume(mixerElem, SND_MIXER_SCHN_FRONT_LEFT, rawVolume) < 0
|
||||
|| snd_mixer_selem_set_playback_volume(mixerElem, SND_MIXER_SCHN_FRONT_RIGHT, rawVolume) < 0)
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::getVolume() - Failed to set mixer volume!";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::getVolume() - Failed to get volume range!";
|
||||
}
|
||||
}
|
||||
#elif defined(WIN32) || defined(_WIN32)
|
||||
if (mixerHandle != nullptr)
|
||||
{
|
||||
//Windows older than Vista. use mixer API. get volume from line control
|
||||
MIXERCONTROLDETAILS_UNSIGNED value;
|
||||
value.dwValue = (volume * 65535) / 100;
|
||||
MIXERCONTROLDETAILS mixerControlDetails;
|
||||
mixerControlDetails.cbStruct = sizeof(MIXERCONTROLDETAILS);
|
||||
mixerControlDetails.dwControlID = mixerControl.dwControlID;
|
||||
mixerControlDetails.cChannels = 1; //always 1 for a MIXERCONTROL_CONTROLF_UNIFORM control
|
||||
mixerControlDetails.cMultipleItems = 0; //always 0 except for a MIXERCONTROL_CONTROLF_MULTIPLE control
|
||||
mixerControlDetails.paDetails = &value;
|
||||
mixerControlDetails.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED);
|
||||
if (mixerSetControlDetails((HMIXEROBJ)mixerHandle, &mixerControlDetails, MIXER_SETCONTROLDETAILSF_VALUE) != MMSYSERR_NOERROR)
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::setVolume() - Failed to set mixer volume!";
|
||||
}
|
||||
}
|
||||
else if (endpointVolume != nullptr)
|
||||
{
|
||||
//Windows Vista or above. use EndpointVolume API
|
||||
float floatVolume = 0.0f; //0-1
|
||||
if (volume > 0) {
|
||||
floatVolume = (float)volume / 100.0f;
|
||||
}
|
||||
if (endpointVolume->SetMasterVolumeLevelScalar(floatVolume, nullptr) != S_OK)
|
||||
{
|
||||
LOG(LogError) << "VolumeControl::setVolume() - Failed to set master volume!";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
58
es-app/src/VolumeControl.h
Normal file
58
es-app/src/VolumeControl.h
Normal file
|
@ -0,0 +1,58 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined (__APPLE__)
|
||||
#error TODO: Not implemented for MacOS yet!!!
|
||||
#elif defined(__linux__)
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <alsa/asoundlib.h>
|
||||
#elif defined(WIN32) || defined(_WIN32)
|
||||
#include <Windows.h>
|
||||
#include <MMSystem.h>
|
||||
#include <mmdeviceapi.h>
|
||||
#include <endpointvolume.h>
|
||||
#endif
|
||||
|
||||
/*!
|
||||
Singleton pattern. Call getInstance() to get an object.
|
||||
*/
|
||||
class VolumeControl
|
||||
{
|
||||
#if defined (__APPLE__)
|
||||
#error TODO: Not implemented for MacOS yet!!!
|
||||
#elif defined(__linux__)
|
||||
static const char * mixerName;
|
||||
static const char * mixerCard;
|
||||
int mixerIndex;
|
||||
snd_mixer_t* mixerHandle;
|
||||
snd_mixer_elem_t* mixerElem;
|
||||
snd_mixer_selem_id_t* mixerSelemId;
|
||||
#elif defined(WIN32) || defined(_WIN32)
|
||||
HMIXER mixerHandle;
|
||||
MIXERCONTROL mixerControl;
|
||||
IAudioEndpointVolume * endpointVolume;
|
||||
#endif
|
||||
|
||||
int originalVolume;
|
||||
int internalVolume;
|
||||
|
||||
static std::weak_ptr<VolumeControl> sInstance;
|
||||
|
||||
VolumeControl();
|
||||
VolumeControl(const VolumeControl & right);
|
||||
VolumeControl & operator=(const VolumeControl & right);
|
||||
|
||||
public:
|
||||
static std::shared_ptr<VolumeControl> & getInstance();
|
||||
|
||||
void init();
|
||||
void deinit();
|
||||
|
||||
int getVolume() const;
|
||||
void setVolume(int volume);
|
||||
|
||||
~VolumeControl();
|
||||
};
|
64
es-app/src/animations/LaunchAnimation.h
Normal file
64
es-app/src/animations/LaunchAnimation.h
Normal file
|
@ -0,0 +1,64 @@
|
|||
#pragma once
|
||||
|
||||
#include "animations/Animation.h"
|
||||
#include "Log.h"
|
||||
|
||||
// let's look at the game launch effect:
|
||||
// -move camera to center on point P (interpolation method: linear)
|
||||
// -zoom camera to factor Z via matrix scale (interpolation method: exponential)
|
||||
// -fade screen to black at rate F (interpolation method: exponential)
|
||||
|
||||
// How the animation gets constructed from the example of implementing the game launch effect:
|
||||
// 1. Current parameters are retrieved through a get() call
|
||||
// ugliness:
|
||||
// -have to have a way to get a reference to the animation
|
||||
// -requires additional implementation in some parent object, potentially AnimationController
|
||||
// 2. ViewController is passed in LaunchAnimation constructor, applies state through setters
|
||||
// ugliness:
|
||||
// -effect only works for ViewController
|
||||
// 3. Pass references to ViewController variables - LaunchAnimation(mCameraPos, mFadePerc, target)
|
||||
// ugliness:
|
||||
// -what if ViewController is deleted? --> AnimationController class handles that
|
||||
// -no callbacks for changes...but that works well with this style of update, so I think it's okay
|
||||
// 4. Use callbacks to set variables
|
||||
// ugliness:
|
||||
// -boilerplate as hell every time
|
||||
|
||||
// #3 wins
|
||||
// can't wait to see how this one bites me in the ass
|
||||
|
||||
class LaunchAnimation : public Animation
|
||||
{
|
||||
public:
|
||||
//Target is a centerpoint
|
||||
LaunchAnimation(Eigen::Affine3f& camera, float& fade, const Eigen::Vector3f& target, int duration) :
|
||||
mCameraStart(camera), mTarget(target), mDuration(duration), cameraOut(camera), fadeOut(fade) {}
|
||||
|
||||
int getDuration() const override { return mDuration; }
|
||||
|
||||
void apply(float t) override
|
||||
{
|
||||
cameraOut = Eigen::Affine3f::Identity();
|
||||
|
||||
float zoom = lerp<float>(1.0, 4.25f, t*t);
|
||||
cameraOut.scale(Eigen::Vector3f(zoom, zoom, 1));
|
||||
|
||||
const float sw = (float)Renderer::getScreenWidth() / zoom;
|
||||
const float sh = (float)Renderer::getScreenHeight() / zoom;
|
||||
|
||||
Eigen::Vector3f centerPoint = lerp<Eigen::Vector3f>(-mCameraStart.translation() + Eigen::Vector3f(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0),
|
||||
mTarget, smoothStep(0.0, 1.0, t));
|
||||
|
||||
cameraOut.translate(Eigen::Vector3f((sw / 2) - centerPoint.x(), (sh / 2) - centerPoint.y(), 0));
|
||||
|
||||
fadeOut = lerp<float>(0.0, 1.0, t*t);
|
||||
}
|
||||
|
||||
private:
|
||||
Eigen::Affine3f mCameraStart;
|
||||
Eigen::Vector3f mTarget;
|
||||
int mDuration;
|
||||
|
||||
Eigen::Affine3f& cameraOut;
|
||||
float& fadeOut;
|
||||
};
|
24
es-app/src/animations/MoveCameraAnimation.h
Normal file
24
es-app/src/animations/MoveCameraAnimation.h
Normal file
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include "animations/Animation.h"
|
||||
|
||||
class MoveCameraAnimation : public Animation
|
||||
{
|
||||
public:
|
||||
MoveCameraAnimation(Eigen::Affine3f& camera, const Eigen::Vector3f& target) : mCameraStart(camera), mTarget(target), cameraOut(camera) {}
|
||||
|
||||
int getDuration() const override { return 400; }
|
||||
|
||||
void apply(float t) override
|
||||
{
|
||||
// cubic ease out
|
||||
t -= 1;
|
||||
cameraOut.translation() = -lerp<Eigen::Vector3f>(-mCameraStart.translation(), mTarget, t*t*t + 1);
|
||||
}
|
||||
|
||||
private:
|
||||
Eigen::Affine3f mCameraStart;
|
||||
Eigen::Vector3f mTarget;
|
||||
|
||||
Eigen::Affine3f& cameraOut;
|
||||
};
|
51
es-app/src/components/AsyncReqComponent.cpp
Normal file
51
es-app/src/components/AsyncReqComponent.cpp
Normal file
|
@ -0,0 +1,51 @@
|
|||
#include "components/AsyncReqComponent.h"
|
||||
#include "Renderer.h"
|
||||
|
||||
AsyncReqComponent::AsyncReqComponent(Window* window, std::shared_ptr<HttpReq> req, std::function<void(std::shared_ptr<HttpReq>)> onSuccess, std::function<void()> onCancel)
|
||||
: GuiComponent(window),
|
||||
mSuccessFunc(onSuccess), mCancelFunc(onCancel), mTime(0), mRequest(req)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
bool AsyncReqComponent::input(InputConfig* config, Input input)
|
||||
{
|
||||
if(input.value != 0 && config->isMappedTo("b", input))
|
||||
{
|
||||
if(mCancelFunc)
|
||||
mCancelFunc();
|
||||
|
||||
delete this;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AsyncReqComponent::update(int deltaTime)
|
||||
{
|
||||
if(mRequest->status() != HttpReq::REQ_IN_PROGRESS)
|
||||
{
|
||||
mSuccessFunc(mRequest);
|
||||
delete this;
|
||||
return;
|
||||
}
|
||||
|
||||
mTime += deltaTime;
|
||||
}
|
||||
|
||||
void AsyncReqComponent::render(const Eigen::Affine3f& parentTrans)
|
||||
{
|
||||
Eigen::Affine3f trans = Eigen::Affine3f::Identity();
|
||||
trans = trans.translate(Eigen::Vector3f(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0));
|
||||
Renderer::setMatrix(trans);
|
||||
|
||||
Eigen::Vector3f point(cos(mTime * 0.01f) * 12, sin(mTime * 0.01f) * 12, 0);
|
||||
Renderer::drawRect((int)point.x(), (int)point.y(), 8, 8, 0x0000FFFF);
|
||||
}
|
||||
|
||||
std::vector<HelpPrompt> AsyncReqComponent::getHelpPrompts()
|
||||
{
|
||||
std::vector<HelpPrompt> prompts;
|
||||
prompts.push_back(HelpPrompt("b", "cancel"));
|
||||
return prompts;
|
||||
}
|
45
es-app/src/components/AsyncReqComponent.h
Normal file
45
es-app/src/components/AsyncReqComponent.h
Normal file
|
@ -0,0 +1,45 @@
|
|||
#pragma once
|
||||
|
||||
#include "GuiComponent.h"
|
||||
#include "HttpReq.h"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
/*
|
||||
Used to asynchronously run an HTTP request.
|
||||
Displays a simple animation on the UI to show the application hasn't frozen. Can be canceled by the user pressing B.
|
||||
|
||||
Usage example:
|
||||
std::shared_ptr<HttpReq> httpreq = std::make_shared<HttpReq>("cdn.garcya.us", "/wp-content/uploads/2010/04/TD250.jpg");
|
||||
AsyncReqComponent* req = new AsyncReqComponent(mWindow, httpreq,
|
||||
[] (std::shared_ptr<HttpReq> r)
|
||||
{
|
||||
LOG(LogInfo) << "Request completed";
|
||||
LOG(LogInfo) << " error, if any: " << r->getErrorMsg();
|
||||
}, [] ()
|
||||
{
|
||||
LOG(LogInfo) << "Request canceled";
|
||||
});
|
||||
|
||||
mWindow->pushGui(req);
|
||||
//we can forget about req, since it will always delete itself
|
||||
*/
|
||||
|
||||
class AsyncReqComponent : public GuiComponent
|
||||
{
|
||||
public:
|
||||
|
||||
AsyncReqComponent(Window* window, std::shared_ptr<HttpReq> req, std::function<void(std::shared_ptr<HttpReq>)> onSuccess, std::function<void()> onCancel = nullptr);
|
||||
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
void update(int deltaTime) override;
|
||||
void render(const Eigen::Affine3f& parentTrans) override;
|
||||
|
||||
virtual std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
private:
|
||||
std::function<void(std::shared_ptr<HttpReq>)> mSuccessFunc;
|
||||
std::function<void()> mCancelFunc;
|
||||
|
||||
unsigned int mTime;
|
||||
std::shared_ptr<HttpReq> mRequest;
|
||||
};
|
169
es-app/src/components/RatingComponent.cpp
Normal file
169
es-app/src/components/RatingComponent.cpp
Normal file
|
@ -0,0 +1,169 @@
|
|||
#include "components/RatingComponent.h"
|
||||
#include "Renderer.h"
|
||||
#include "Window.h"
|
||||
#include "Util.h"
|
||||
#include "resources/SVGResource.h"
|
||||
|
||||
RatingComponent::RatingComponent(Window* window) : GuiComponent(window)
|
||||
{
|
||||
mFilledTexture = TextureResource::get(":/star_filled.svg", true);
|
||||
mUnfilledTexture = TextureResource::get(":/star_unfilled.svg", true);
|
||||
mValue = 0.5f;
|
||||
mSize << 64 * NUM_RATING_STARS, 64;
|
||||
updateVertices();
|
||||
}
|
||||
|
||||
void RatingComponent::setValue(const std::string& value)
|
||||
{
|
||||
if(value.empty())
|
||||
{
|
||||
mValue = 0.0f;
|
||||
}else{
|
||||
mValue = stof(value);
|
||||
if(mValue > 1.0f)
|
||||
mValue = 1.0f;
|
||||
else if(mValue < 0.0f)
|
||||
mValue = 0.0f;
|
||||
}
|
||||
|
||||
updateVertices();
|
||||
}
|
||||
|
||||
std::string RatingComponent::getValue() const
|
||||
{
|
||||
return std::to_string((long double)mValue);
|
||||
}
|
||||
|
||||
void RatingComponent::onSizeChanged()
|
||||
{
|
||||
if(mSize.y() == 0)
|
||||
mSize[1] = mSize.x() / NUM_RATING_STARS;
|
||||
else if(mSize.x() == 0)
|
||||
mSize[0] = mSize.y() * NUM_RATING_STARS;
|
||||
|
||||
auto filledSVG = dynamic_cast<SVGResource*>(mFilledTexture.get());
|
||||
auto unfilledSVG = dynamic_cast<SVGResource*>(mUnfilledTexture.get());
|
||||
|
||||
if(mSize.y() > 0)
|
||||
{
|
||||
size_t heightPx = (size_t)round(mSize.y());
|
||||
if(filledSVG)
|
||||
filledSVG->rasterizeAt(heightPx, heightPx);
|
||||
if(unfilledSVG)
|
||||
unfilledSVG->rasterizeAt(heightPx, heightPx);
|
||||
}
|
||||
|
||||
updateVertices();
|
||||
}
|
||||
|
||||
void RatingComponent::updateVertices()
|
||||
{
|
||||
const float numStars = NUM_RATING_STARS;
|
||||
|
||||
const float h = round(getSize().y()); // is the same as a single star's width
|
||||
const float w = round(h * mValue * numStars);
|
||||
const float fw = round(h * numStars);
|
||||
|
||||
mVertices[0].pos << 0.0f, 0.0f;
|
||||
mVertices[0].tex << 0.0f, 1.0f;
|
||||
mVertices[1].pos << w, h;
|
||||
mVertices[1].tex << mValue * numStars, 0.0f;
|
||||
mVertices[2].pos << 0.0f, h;
|
||||
mVertices[2].tex << 0.0f, 0.0f;
|
||||
|
||||
mVertices[3] = mVertices[0];
|
||||
mVertices[4].pos << w, 0.0f;
|
||||
mVertices[4].tex << mValue * numStars, 1.0f;
|
||||
mVertices[5] = mVertices[1];
|
||||
|
||||
mVertices[6] = mVertices[4];
|
||||
mVertices[7].pos << fw, h;
|
||||
mVertices[7].tex << numStars, 0.0f;
|
||||
mVertices[8] = mVertices[1];
|
||||
|
||||
mVertices[9] = mVertices[6];
|
||||
mVertices[10].pos << fw, 0.0f;
|
||||
mVertices[10].tex << numStars, 1.0f;
|
||||
mVertices[11] = mVertices[7];
|
||||
}
|
||||
|
||||
void RatingComponent::render(const Eigen::Affine3f& parentTrans)
|
||||
{
|
||||
Eigen::Affine3f trans = roundMatrix(parentTrans * getTransform());
|
||||
Renderer::setMatrix(trans);
|
||||
|
||||
glEnable(GL_TEXTURE_2D);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
glColor4ub(255, 255, 255, getOpacity());
|
||||
|
||||
glEnableClientState(GL_VERTEX_ARRAY);
|
||||
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
|
||||
|
||||
glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].pos);
|
||||
glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].tex);
|
||||
|
||||
mFilledTexture->bind();
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
|
||||
mUnfilledTexture->bind();
|
||||
glDrawArrays(GL_TRIANGLES, 6, 6);
|
||||
|
||||
glDisableClientState(GL_VERTEX_ARRAY);
|
||||
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
|
||||
|
||||
glDisable(GL_TEXTURE_2D);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
glColor4ub(255, 255, 255, 255);
|
||||
|
||||
renderChildren(trans);
|
||||
}
|
||||
|
||||
bool RatingComponent::input(InputConfig* config, Input input)
|
||||
{
|
||||
if(config->isMappedTo("a", input) && input.value != 0)
|
||||
{
|
||||
mValue += 1.f / NUM_RATING_STARS;
|
||||
if(mValue > 1.0f)
|
||||
mValue = 0.0f;
|
||||
|
||||
updateVertices();
|
||||
}
|
||||
|
||||
return GuiComponent::input(config, input);
|
||||
}
|
||||
|
||||
void RatingComponent::applyTheme(const std::shared_ptr<ThemeData>& theme, const std::string& view, const std::string& element, unsigned int properties)
|
||||
{
|
||||
GuiComponent::applyTheme(theme, view, element, properties);
|
||||
|
||||
using namespace ThemeFlags;
|
||||
|
||||
const ThemeData::ThemeElement* elem = theme->getElement(view, element, "rating");
|
||||
if(!elem)
|
||||
return;
|
||||
|
||||
bool imgChanged = false;
|
||||
if(properties & PATH && elem->has("filledPath"))
|
||||
{
|
||||
mFilledTexture = TextureResource::get(elem->get<std::string>("filledPath"), true);
|
||||
imgChanged = true;
|
||||
}
|
||||
if(properties & PATH && elem->has("unfilledPath"))
|
||||
{
|
||||
mUnfilledTexture = TextureResource::get(elem->get<std::string>("unfilledPath"), true);
|
||||
imgChanged = true;
|
||||
}
|
||||
|
||||
if(imgChanged)
|
||||
onSizeChanged();
|
||||
}
|
||||
|
||||
std::vector<HelpPrompt> RatingComponent::getHelpPrompts()
|
||||
{
|
||||
std::vector<HelpPrompt> prompts;
|
||||
prompts.push_back(HelpPrompt("a", "add star"));
|
||||
return prompts;
|
||||
}
|
44
es-app/src/components/RatingComponent.h
Normal file
44
es-app/src/components/RatingComponent.h
Normal file
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
|
||||
#include "GuiComponent.h"
|
||||
#include "resources/TextureResource.h"
|
||||
|
||||
#define NUM_RATING_STARS 5
|
||||
|
||||
// Used to visually display/edit some sort of "score" - e.g. 5/10, 3/5, etc.
|
||||
// setSize(x, y) works a little differently than you might expect:
|
||||
// * (0, y != 0) - x will be automatically calculated (5*y).
|
||||
// * (x != 0, 0) - y will be automatically calculated (x/5).
|
||||
// * (x != 0, y != 0) - you better be sure x = y*5
|
||||
class RatingComponent : public GuiComponent
|
||||
{
|
||||
public:
|
||||
RatingComponent(Window* window);
|
||||
|
||||
std::string getValue() const override;
|
||||
void setValue(const std::string& value) override; // Should be a normalized float (in the range [0..1]) - if it's not, it will be clamped.
|
||||
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
void render(const Eigen::Affine3f& parentTrans);
|
||||
|
||||
void onSizeChanged() override;
|
||||
|
||||
virtual void applyTheme(const std::shared_ptr<ThemeData>& theme, const std::string& view, const std::string& element, unsigned int properties) override;
|
||||
|
||||
virtual std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
|
||||
private:
|
||||
void updateVertices();
|
||||
|
||||
float mValue;
|
||||
|
||||
struct Vertex
|
||||
{
|
||||
Eigen::Vector2f pos;
|
||||
Eigen::Vector2f tex;
|
||||
} mVertices[12];
|
||||
|
||||
std::shared_ptr<TextureResource> mFilledTexture;
|
||||
std::shared_ptr<TextureResource> mUnfilledTexture;
|
||||
};
|
||||
|
476
es-app/src/components/ScraperSearchComponent.cpp
Normal file
476
es-app/src/components/ScraperSearchComponent.cpp
Normal file
|
@ -0,0 +1,476 @@
|
|||
#include "components/ScraperSearchComponent.h"
|
||||
|
||||
#include "guis/GuiMsgBox.h"
|
||||
#include "components/TextComponent.h"
|
||||
#include "components/ScrollableContainer.h"
|
||||
#include "components/ImageComponent.h"
|
||||
#include "components/RatingComponent.h"
|
||||
#include "components/DateTimeComponent.h"
|
||||
#include "components/AnimatedImageComponent.h"
|
||||
#include "components/ComponentList.h"
|
||||
#include "HttpReq.h"
|
||||
#include "Settings.h"
|
||||
#include "Log.h"
|
||||
#include "Util.h"
|
||||
#include "guis/GuiTextEditPopup.h"
|
||||
|
||||
ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) : GuiComponent(window),
|
||||
mGrid(window, Eigen::Vector2i(4, 3)), mBusyAnim(window),
|
||||
mSearchType(type)
|
||||
{
|
||||
addChild(&mGrid);
|
||||
|
||||
mBlockAccept = false;
|
||||
|
||||
using namespace Eigen;
|
||||
|
||||
// left spacer (empty component, needed for borders)
|
||||
mGrid.setEntry(std::make_shared<GuiComponent>(mWindow), Vector2i(0, 0), false, false, Vector2i(1, 3), GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM);
|
||||
|
||||
// selected result name
|
||||
mResultName = std::make_shared<TextComponent>(mWindow, "Result name", Font::get(FONT_SIZE_MEDIUM), 0x777777FF);
|
||||
|
||||
// selected result thumbnail
|
||||
mResultThumbnail = std::make_shared<ImageComponent>(mWindow);
|
||||
mGrid.setEntry(mResultThumbnail, Vector2i(1, 1), false, false, Vector2i(1, 1));
|
||||
|
||||
// selected result desc + container
|
||||
mDescContainer = std::make_shared<ScrollableContainer>(mWindow);
|
||||
mResultDesc = std::make_shared<TextComponent>(mWindow, "Result desc", Font::get(FONT_SIZE_SMALL), 0x777777FF);
|
||||
mDescContainer->addChild(mResultDesc.get());
|
||||
mDescContainer->setAutoScroll(true);
|
||||
|
||||
// metadata
|
||||
auto font = Font::get(FONT_SIZE_SMALL); // this gets replaced in onSizeChanged() so its just a placeholder
|
||||
const unsigned int mdColor = 0x777777FF;
|
||||
const unsigned int mdLblColor = 0x666666FF;
|
||||
mMD_Rating = std::make_shared<RatingComponent>(mWindow);
|
||||
mMD_ReleaseDate = std::make_shared<DateTimeComponent>(mWindow);
|
||||
mMD_ReleaseDate->setColor(mdColor);
|
||||
mMD_Developer = std::make_shared<TextComponent>(mWindow, "", font, mdColor);
|
||||
mMD_Publisher = std::make_shared<TextComponent>(mWindow, "", font, mdColor);
|
||||
mMD_Genre = std::make_shared<TextComponent>(mWindow, "", font, mdColor);
|
||||
mMD_Players = std::make_shared<TextComponent>(mWindow, "", font, mdColor);
|
||||
|
||||
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "RATING:", font, mdLblColor), mMD_Rating, false));
|
||||
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "RELEASED:", font, mdLblColor), mMD_ReleaseDate));
|
||||
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "DEVELOPER:", font, mdLblColor), mMD_Developer));
|
||||
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "PUBLISHER:", font, mdLblColor), mMD_Publisher));
|
||||
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "GENRE:", font, mdLblColor), mMD_Genre));
|
||||
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "PLAYERS:", font, mdLblColor), mMD_Players));
|
||||
|
||||
mMD_Grid = std::make_shared<ComponentGrid>(mWindow, Vector2i(2, mMD_Pairs.size()*2 - 1));
|
||||
unsigned int i = 0;
|
||||
for(auto it = mMD_Pairs.begin(); it != mMD_Pairs.end(); it++)
|
||||
{
|
||||
mMD_Grid->setEntry(it->first, Vector2i(0, i), false, true);
|
||||
mMD_Grid->setEntry(it->second, Vector2i(1, i), false, it->resize);
|
||||
i += 2;
|
||||
}
|
||||
|
||||
mGrid.setEntry(mMD_Grid, Vector2i(2, 1), false, false);
|
||||
|
||||
// result list
|
||||
mResultList = std::make_shared<ComponentList>(mWindow);
|
||||
mResultList->setCursorChangedCallback([this](CursorState state) { if(state == CURSOR_STOPPED) updateInfoPane(); });
|
||||
|
||||
updateViewStyle();
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::onSizeChanged()
|
||||
{
|
||||
mGrid.setSize(mSize);
|
||||
|
||||
if(mSize.x() == 0 || mSize.y() == 0)
|
||||
return;
|
||||
|
||||
// column widths
|
||||
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT)
|
||||
mGrid.setColWidthPerc(0, 0.02f); // looks better when this is higher in auto mode
|
||||
else
|
||||
mGrid.setColWidthPerc(0, 0.01f);
|
||||
|
||||
mGrid.setColWidthPerc(1, 0.25f);
|
||||
mGrid.setColWidthPerc(2, 0.25f);
|
||||
|
||||
// row heights
|
||||
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT) // show name
|
||||
mGrid.setRowHeightPerc(0, (mResultName->getFont()->getHeight() * 1.6f) / mGrid.getSize().y()); // result name
|
||||
else
|
||||
mGrid.setRowHeightPerc(0, 0.0825f); // hide name but do padding
|
||||
|
||||
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT)
|
||||
{
|
||||
mGrid.setRowHeightPerc(2, 0.2f);
|
||||
}else{
|
||||
mGrid.setRowHeightPerc(1, 0.505f);
|
||||
}
|
||||
|
||||
const float boxartCellScale = 0.9f;
|
||||
|
||||
// limit thumbnail size using setMaxHeight - we do this instead of letting mGrid call setSize because it maintains the aspect ratio
|
||||
// we also pad a little so it doesn't rub up against the metadata labels
|
||||
mResultThumbnail->setMaxSize(mGrid.getColWidth(1) * boxartCellScale, mGrid.getRowHeight(1));
|
||||
|
||||
// metadata
|
||||
resizeMetadata();
|
||||
|
||||
if(mSearchType != ALWAYS_ACCEPT_FIRST_RESULT)
|
||||
mDescContainer->setSize(mGrid.getColWidth(1)*boxartCellScale + mGrid.getColWidth(2), mResultDesc->getFont()->getHeight() * 3);
|
||||
else
|
||||
mDescContainer->setSize(mGrid.getColWidth(3)*boxartCellScale, mResultDesc->getFont()->getHeight() * 8);
|
||||
|
||||
mResultDesc->setSize(mDescContainer->getSize().x(), 0); // make desc text wrap at edge of container
|
||||
|
||||
mGrid.onSizeChanged();
|
||||
|
||||
mBusyAnim.setSize(mSize);
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::resizeMetadata()
|
||||
{
|
||||
mMD_Grid->setSize(mGrid.getColWidth(2), mGrid.getRowHeight(1));
|
||||
if(mMD_Grid->getSize().y() > mMD_Pairs.size())
|
||||
{
|
||||
const int fontHeight = (int)(mMD_Grid->getSize().y() / mMD_Pairs.size() * 0.8f);
|
||||
auto fontLbl = Font::get(fontHeight, FONT_PATH_REGULAR);
|
||||
auto fontComp = Font::get(fontHeight, FONT_PATH_LIGHT);
|
||||
|
||||
// update label fonts
|
||||
float maxLblWidth = 0;
|
||||
for(auto it = mMD_Pairs.begin(); it != mMD_Pairs.end(); it++)
|
||||
{
|
||||
it->first->setFont(fontLbl);
|
||||
it->first->setSize(0, 0);
|
||||
if(it->first->getSize().x() > maxLblWidth)
|
||||
maxLblWidth = it->first->getSize().x() + 6;
|
||||
}
|
||||
|
||||
for(unsigned int i = 0; i < mMD_Pairs.size(); i++)
|
||||
{
|
||||
mMD_Grid->setRowHeightPerc(i*2, (fontLbl->getLetterHeight() + 2) / mMD_Grid->getSize().y());
|
||||
}
|
||||
|
||||
// update component fonts
|
||||
mMD_ReleaseDate->setFont(fontComp);
|
||||
mMD_Developer->setFont(fontComp);
|
||||
mMD_Publisher->setFont(fontComp);
|
||||
mMD_Genre->setFont(fontComp);
|
||||
mMD_Players->setFont(fontComp);
|
||||
|
||||
mMD_Grid->setColWidthPerc(0, maxLblWidth / mMD_Grid->getSize().x());
|
||||
|
||||
// rating is manually sized
|
||||
mMD_Rating->setSize(mMD_Grid->getColWidth(1), fontLbl->getHeight() * 0.65f);
|
||||
mMD_Grid->onSizeChanged();
|
||||
|
||||
// make result font follow label font
|
||||
mResultDesc->setFont(Font::get(fontHeight, FONT_PATH_REGULAR));
|
||||
}
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::updateViewStyle()
|
||||
{
|
||||
using namespace Eigen;
|
||||
|
||||
// unlink description and result list and result name
|
||||
mGrid.removeEntry(mResultName);
|
||||
mGrid.removeEntry(mResultDesc);
|
||||
mGrid.removeEntry(mResultList);
|
||||
|
||||
// add them back depending on search type
|
||||
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT)
|
||||
{
|
||||
// show name
|
||||
mGrid.setEntry(mResultName, Vector2i(1, 0), false, true, Vector2i(2, 1), GridFlags::BORDER_TOP);
|
||||
|
||||
// need a border on the bottom left
|
||||
mGrid.setEntry(std::make_shared<GuiComponent>(mWindow), Vector2i(0, 2), false, false, Vector2i(3, 1), GridFlags::BORDER_BOTTOM);
|
||||
|
||||
// show description on the right
|
||||
mGrid.setEntry(mDescContainer, Vector2i(3, 0), false, false, Vector2i(1, 3), GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM);
|
||||
mResultDesc->setSize(mDescContainer->getSize().x(), 0); // make desc text wrap at edge of container
|
||||
}else{
|
||||
// fake row where name would be
|
||||
mGrid.setEntry(std::make_shared<GuiComponent>(mWindow), Vector2i(1, 0), false, true, Vector2i(2, 1), GridFlags::BORDER_TOP);
|
||||
|
||||
// show result list on the right
|
||||
mGrid.setEntry(mResultList, Vector2i(3, 0), true, true, Vector2i(1, 3), GridFlags::BORDER_LEFT | GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM);
|
||||
|
||||
// show description under image/info
|
||||
mGrid.setEntry(mDescContainer, Vector2i(1, 2), false, false, Vector2i(2, 1), GridFlags::BORDER_BOTTOM);
|
||||
mResultDesc->setSize(mDescContainer->getSize().x(), 0); // make desc text wrap at edge of container
|
||||
}
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::search(const ScraperSearchParams& params)
|
||||
{
|
||||
mResultList->clear();
|
||||
mScraperResults.clear();
|
||||
mThumbnailReq.reset();
|
||||
mMDResolveHandle.reset();
|
||||
updateInfoPane();
|
||||
|
||||
mLastSearch = params;
|
||||
mSearchHandle = startScraperSearch(params);
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::stop()
|
||||
{
|
||||
mThumbnailReq.reset();
|
||||
mSearchHandle.reset();
|
||||
mMDResolveHandle.reset();
|
||||
mBlockAccept = false;
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::onSearchDone(const std::vector<ScraperSearchResult>& results)
|
||||
{
|
||||
mResultList->clear();
|
||||
|
||||
mScraperResults = results;
|
||||
|
||||
const int end = results.size() > MAX_SCRAPER_RESULTS ? MAX_SCRAPER_RESULTS : results.size(); // at max display 5
|
||||
|
||||
auto font = Font::get(FONT_SIZE_MEDIUM);
|
||||
unsigned int color = 0x777777FF;
|
||||
if(end == 0)
|
||||
{
|
||||
ComponentListRow row;
|
||||
row.addElement(std::make_shared<TextComponent>(mWindow, "NO GAMES FOUND - SKIP", font, color), true);
|
||||
|
||||
if(mSkipCallback)
|
||||
row.makeAcceptInputHandler(mSkipCallback);
|
||||
|
||||
mResultList->addRow(row);
|
||||
mGrid.resetCursor();
|
||||
}else{
|
||||
ComponentListRow row;
|
||||
for(int i = 0; i < end; i++)
|
||||
{
|
||||
row.elements.clear();
|
||||
row.addElement(std::make_shared<TextComponent>(mWindow, strToUpper(results.at(i).mdl.get("name")), font, color), true);
|
||||
row.makeAcceptInputHandler([this, i] { returnResult(mScraperResults.at(i)); });
|
||||
mResultList->addRow(row);
|
||||
}
|
||||
mGrid.resetCursor();
|
||||
}
|
||||
|
||||
mBlockAccept = false;
|
||||
updateInfoPane();
|
||||
|
||||
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT)
|
||||
{
|
||||
if(mScraperResults.size() == 0)
|
||||
mSkipCallback();
|
||||
else
|
||||
returnResult(mScraperResults.front());
|
||||
}else if(mSearchType == ALWAYS_ACCEPT_MATCHING_CRC)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::onSearchError(const std::string& error)
|
||||
{
|
||||
LOG(LogInfo) << "ScraperSearchComponent search error: " << error;
|
||||
mWindow->pushGui(new GuiMsgBox(mWindow, strToUpper(error),
|
||||
"RETRY", std::bind(&ScraperSearchComponent::search, this, mLastSearch),
|
||||
"SKIP", mSkipCallback,
|
||||
"CANCEL", mCancelCallback));
|
||||
}
|
||||
|
||||
int ScraperSearchComponent::getSelectedIndex()
|
||||
{
|
||||
if(!mScraperResults.size() || mGrid.getSelectedComponent() != mResultList)
|
||||
return -1;
|
||||
|
||||
return mResultList->getCursorId();
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::updateInfoPane()
|
||||
{
|
||||
int i = getSelectedIndex();
|
||||
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT && mScraperResults.size())
|
||||
{
|
||||
i = 0;
|
||||
}
|
||||
|
||||
if(i != -1 && (int)mScraperResults.size() > i)
|
||||
{
|
||||
ScraperSearchResult& res = mScraperResults.at(i);
|
||||
mResultName->setText(strToUpper(res.mdl.get("name")));
|
||||
mResultDesc->setText(strToUpper(res.mdl.get("desc")));
|
||||
mDescContainer->reset();
|
||||
|
||||
mResultThumbnail->setImage("");
|
||||
const std::string& thumb = res.thumbnailUrl.empty() ? res.imageUrl : res.thumbnailUrl;
|
||||
if(!thumb.empty())
|
||||
{
|
||||
mThumbnailReq = std::unique_ptr<HttpReq>(new HttpReq(thumb));
|
||||
}else{
|
||||
mThumbnailReq.reset();
|
||||
}
|
||||
|
||||
// metadata
|
||||
mMD_Rating->setValue(strToUpper(res.mdl.get("rating")));
|
||||
mMD_ReleaseDate->setValue(strToUpper(res.mdl.get("releasedate")));
|
||||
mMD_Developer->setText(strToUpper(res.mdl.get("developer")));
|
||||
mMD_Publisher->setText(strToUpper(res.mdl.get("publisher")));
|
||||
mMD_Genre->setText(strToUpper(res.mdl.get("genre")));
|
||||
mMD_Players->setText(strToUpper(res.mdl.get("players")));
|
||||
mGrid.onSizeChanged();
|
||||
}else{
|
||||
mResultName->setText("");
|
||||
mResultDesc->setText("");
|
||||
mResultThumbnail->setImage("");
|
||||
|
||||
// metadata
|
||||
mMD_Rating->setValue("");
|
||||
mMD_ReleaseDate->setValue("");
|
||||
mMD_Developer->setText("");
|
||||
mMD_Publisher->setText("");
|
||||
mMD_Genre->setText("");
|
||||
mMD_Players->setText("");
|
||||
}
|
||||
}
|
||||
|
||||
bool ScraperSearchComponent::input(InputConfig* config, Input input)
|
||||
{
|
||||
if(config->isMappedTo("a", input) && input.value != 0)
|
||||
{
|
||||
if(mBlockAccept)
|
||||
return true;
|
||||
}
|
||||
|
||||
return GuiComponent::input(config, input);
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::render(const Eigen::Affine3f& parentTrans)
|
||||
{
|
||||
Eigen::Affine3f trans = parentTrans * getTransform();
|
||||
|
||||
renderChildren(trans);
|
||||
|
||||
if(mBlockAccept)
|
||||
{
|
||||
Renderer::setMatrix(trans);
|
||||
Renderer::drawRect(0.f, 0.f, mSize.x(), mSize.y(), 0x00000011);
|
||||
//Renderer::drawRect((int)mResultList->getPosition().x(), (int)mResultList->getPosition().y(),
|
||||
// (int)mResultList->getSize().x(), (int)mResultList->getSize().y(), 0x00000011);
|
||||
|
||||
mBusyAnim.render(trans);
|
||||
}
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::returnResult(ScraperSearchResult result)
|
||||
{
|
||||
mBlockAccept = true;
|
||||
|
||||
// resolve metadata image before returning
|
||||
if(!result.imageUrl.empty())
|
||||
{
|
||||
mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch);
|
||||
return;
|
||||
}
|
||||
|
||||
mAcceptCallback(result);
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::update(int deltaTime)
|
||||
{
|
||||
GuiComponent::update(deltaTime);
|
||||
|
||||
if(mBlockAccept)
|
||||
{
|
||||
mBusyAnim.update(deltaTime);
|
||||
}
|
||||
|
||||
if(mThumbnailReq && mThumbnailReq->status() != HttpReq::REQ_IN_PROGRESS)
|
||||
{
|
||||
updateThumbnail();
|
||||
}
|
||||
|
||||
if(mSearchHandle && mSearchHandle->status() != ASYNC_IN_PROGRESS)
|
||||
{
|
||||
auto status = mSearchHandle->status();
|
||||
auto results = mSearchHandle->getResults();
|
||||
auto statusString = mSearchHandle->getStatusString();
|
||||
|
||||
// we reset here because onSearchDone in auto mode can call mSkipCallback() which can call
|
||||
// another search() which will set our mSearchHandle to something important
|
||||
mSearchHandle.reset();
|
||||
|
||||
if(status == ASYNC_DONE)
|
||||
{
|
||||
onSearchDone(results);
|
||||
}else if(status == ASYNC_ERROR)
|
||||
{
|
||||
onSearchError(statusString);
|
||||
}
|
||||
}
|
||||
|
||||
if(mMDResolveHandle && mMDResolveHandle->status() != ASYNC_IN_PROGRESS)
|
||||
{
|
||||
if(mMDResolveHandle->status() == ASYNC_DONE)
|
||||
{
|
||||
ScraperSearchResult result = mMDResolveHandle->getResult();
|
||||
mMDResolveHandle.reset();
|
||||
|
||||
// this might end in us being deleted, depending on mAcceptCallback - so make sure this is the last thing we do in update()
|
||||
returnResult(result);
|
||||
}else if(mMDResolveHandle->status() == ASYNC_ERROR)
|
||||
{
|
||||
onSearchError(mMDResolveHandle->getStatusString());
|
||||
mMDResolveHandle.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::updateThumbnail()
|
||||
{
|
||||
if(mThumbnailReq && mThumbnailReq->status() == HttpReq::REQ_SUCCESS)
|
||||
{
|
||||
std::string content = mThumbnailReq->getContent();
|
||||
mResultThumbnail->setImage(content.data(), content.length());
|
||||
mGrid.onSizeChanged(); // a hack to fix the thumbnail position since its size changed
|
||||
}else{
|
||||
LOG(LogWarning) << "thumbnail req failed: " << mThumbnailReq->getErrorMsg();
|
||||
mResultThumbnail->setImage("");
|
||||
}
|
||||
|
||||
mThumbnailReq.reset();
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::openInputScreen(ScraperSearchParams& params)
|
||||
{
|
||||
auto searchForFunc = [&](const std::string& name)
|
||||
{
|
||||
params.nameOverride = name;
|
||||
search(params);
|
||||
};
|
||||
|
||||
stop();
|
||||
mWindow->pushGui(new GuiTextEditPopup(mWindow, "SEARCH FOR",
|
||||
// initial value is last search if there was one, otherwise the clean path name
|
||||
params.nameOverride.empty() ? params.game->getCleanName() : params.nameOverride,
|
||||
searchForFunc, false, "SEARCH"));
|
||||
}
|
||||
|
||||
std::vector<HelpPrompt> ScraperSearchComponent::getHelpPrompts()
|
||||
{
|
||||
std::vector<HelpPrompt> prompts = mGrid.getHelpPrompts();
|
||||
if(getSelectedIndex() != -1)
|
||||
prompts.push_back(HelpPrompt("a", "accept result"));
|
||||
|
||||
return prompts;
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::onFocusGained()
|
||||
{
|
||||
mGrid.onFocusGained();
|
||||
}
|
||||
|
||||
void ScraperSearchComponent::onFocusLost()
|
||||
{
|
||||
mGrid.onFocusLost();
|
||||
}
|
104
es-app/src/components/ScraperSearchComponent.h
Normal file
104
es-app/src/components/ScraperSearchComponent.h
Normal file
|
@ -0,0 +1,104 @@
|
|||
#pragma once
|
||||
|
||||
#include "GuiComponent.h"
|
||||
#include "scrapers/Scraper.h"
|
||||
#include "components/ComponentGrid.h"
|
||||
#include "components/BusyComponent.h"
|
||||
#include <functional>
|
||||
|
||||
class ComponentList;
|
||||
class ImageComponent;
|
||||
class RatingComponent;
|
||||
class TextComponent;
|
||||
class DateTimeComponent;
|
||||
class ScrollableContainer;
|
||||
class HttpReq;
|
||||
class AnimatedImageComponent;
|
||||
|
||||
class ScraperSearchComponent : public GuiComponent
|
||||
{
|
||||
public:
|
||||
enum SearchType
|
||||
{
|
||||
ALWAYS_ACCEPT_FIRST_RESULT,
|
||||
ALWAYS_ACCEPT_MATCHING_CRC,
|
||||
NEVER_AUTO_ACCEPT
|
||||
};
|
||||
|
||||
ScraperSearchComponent(Window* window, SearchType searchType = NEVER_AUTO_ACCEPT);
|
||||
|
||||
void search(const ScraperSearchParams& params);
|
||||
void openInputScreen(ScraperSearchParams& from);
|
||||
void stop();
|
||||
inline SearchType getSearchType() const { return mSearchType; }
|
||||
|
||||
// Metadata assets will be resolved before calling the accept callback (e.g. result.mdl's "image" is automatically downloaded and properly set).
|
||||
inline void setAcceptCallback(const std::function<void(const ScraperSearchResult&)>& acceptCallback) { mAcceptCallback = acceptCallback; }
|
||||
inline void setSkipCallback(const std::function<void()>& skipCallback) { mSkipCallback = skipCallback; };
|
||||
inline void setCancelCallback(const std::function<void()>& cancelCallback) { mCancelCallback = cancelCallback; }
|
||||
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
void update(int deltaTime) override;
|
||||
void render(const Eigen::Affine3f& parentTrans) override;
|
||||
std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
void onSizeChanged() override;
|
||||
void onFocusGained() override;
|
||||
void onFocusLost() override;
|
||||
|
||||
private:
|
||||
void updateViewStyle();
|
||||
void updateThumbnail();
|
||||
void updateInfoPane();
|
||||
|
||||
void resizeMetadata();
|
||||
|
||||
void onSearchError(const std::string& error);
|
||||
void onSearchDone(const std::vector<ScraperSearchResult>& results);
|
||||
|
||||
int getSelectedIndex();
|
||||
|
||||
// resolve any metadata assets that need to be downloaded and return
|
||||
void returnResult(ScraperSearchResult result);
|
||||
|
||||
ComponentGrid mGrid;
|
||||
|
||||
std::shared_ptr<TextComponent> mResultName;
|
||||
std::shared_ptr<ScrollableContainer> mDescContainer;
|
||||
std::shared_ptr<TextComponent> mResultDesc;
|
||||
std::shared_ptr<ImageComponent> mResultThumbnail;
|
||||
std::shared_ptr<ComponentList> mResultList;
|
||||
|
||||
std::shared_ptr<ComponentGrid> mMD_Grid;
|
||||
std::shared_ptr<RatingComponent> mMD_Rating;
|
||||
std::shared_ptr<DateTimeComponent> mMD_ReleaseDate;
|
||||
std::shared_ptr<TextComponent> mMD_Developer;
|
||||
std::shared_ptr<TextComponent> mMD_Publisher;
|
||||
std::shared_ptr<TextComponent> mMD_Genre;
|
||||
std::shared_ptr<TextComponent> mMD_Players;
|
||||
|
||||
// label-component pair
|
||||
struct MetaDataPair
|
||||
{
|
||||
std::shared_ptr<TextComponent> first;
|
||||
std::shared_ptr<GuiComponent> second;
|
||||
bool resize;
|
||||
|
||||
MetaDataPair(const std::shared_ptr<TextComponent>& f, const std::shared_ptr<GuiComponent>& s, bool r = true) : first(f), second(s), resize(r) {};
|
||||
};
|
||||
|
||||
std::vector<MetaDataPair> mMD_Pairs;
|
||||
|
||||
SearchType mSearchType;
|
||||
ScraperSearchParams mLastSearch;
|
||||
std::function<void(const ScraperSearchResult&)> mAcceptCallback;
|
||||
std::function<void()> mSkipCallback;
|
||||
std::function<void()> mCancelCallback;
|
||||
bool mBlockAccept;
|
||||
|
||||
std::unique_ptr<ScraperSearchHandle> mSearchHandle;
|
||||
std::unique_ptr<MDResolveHandle> mMDResolveHandle;
|
||||
std::vector<ScraperSearchResult> mScraperResults;
|
||||
std::unique_ptr<HttpReq> mThumbnailReq;
|
||||
|
||||
BusyComponent mBusyAnim;
|
||||
};
|
369
es-app/src/components/TextListComponent.h
Normal file
369
es-app/src/components/TextListComponent.h
Normal file
|
@ -0,0 +1,369 @@
|
|||
#pragma once
|
||||
|
||||
#include "components/IList.h"
|
||||
#include "Renderer.h"
|
||||
#include "resources/Font.h"
|
||||
#include "InputManager.h"
|
||||
#include "Sound.h"
|
||||
#include "Log.h"
|
||||
#include "ThemeData.h"
|
||||
#include "Util.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
|
||||
struct TextListData
|
||||
{
|
||||
unsigned int colorId;
|
||||
std::shared_ptr<TextCache> textCache;
|
||||
};
|
||||
|
||||
//A graphical list. Supports multiple colors for rows and scrolling.
|
||||
template <typename T>
|
||||
class TextListComponent : public IList<TextListData, T>
|
||||
{
|
||||
protected:
|
||||
using IList<TextListData, T>::mEntries;
|
||||
using IList<TextListData, T>::listUpdate;
|
||||
using IList<TextListData, T>::listInput;
|
||||
using IList<TextListData, T>::listRenderTitleOverlay;
|
||||
using IList<TextListData, T>::getTransform;
|
||||
using IList<TextListData, T>::mSize;
|
||||
using IList<TextListData, T>::mCursor;
|
||||
using IList<TextListData, T>::Entry;
|
||||
|
||||
public:
|
||||
using IList<TextListData, T>::size;
|
||||
using IList<TextListData, T>::isScrolling;
|
||||
using IList<TextListData, T>::stopScrolling;
|
||||
|
||||
TextListComponent(Window* window);
|
||||
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
void update(int deltaTime) override;
|
||||
void render(const Eigen::Affine3f& parentTrans) override;
|
||||
void applyTheme(const std::shared_ptr<ThemeData>& theme, const std::string& view, const std::string& element, unsigned int properties) override;
|
||||
|
||||
void add(const std::string& name, const T& obj, unsigned int colorId);
|
||||
|
||||
enum Alignment
|
||||
{
|
||||
ALIGN_LEFT,
|
||||
ALIGN_CENTER,
|
||||
ALIGN_RIGHT
|
||||
};
|
||||
|
||||
inline void setAlignment(Alignment align) { mAlignment = align; }
|
||||
|
||||
inline void setCursorChangedCallback(const std::function<void(CursorState state)>& func) { mCursorChangedCallback = func; }
|
||||
|
||||
inline void setFont(const std::shared_ptr<Font>& font)
|
||||
{
|
||||
mFont = font;
|
||||
for(auto it = mEntries.begin(); it != mEntries.end(); it++)
|
||||
it->data.textCache.reset();
|
||||
}
|
||||
|
||||
inline void setUppercase(bool uppercase)
|
||||
{
|
||||
mUppercase = true;
|
||||
for(auto it = mEntries.begin(); it != mEntries.end(); it++)
|
||||
it->data.textCache.reset();
|
||||
}
|
||||
|
||||
inline void setSelectorColor(unsigned int color) { mSelectorColor = color; }
|
||||
inline void setSelectedColor(unsigned int color) { mSelectedColor = color; }
|
||||
inline void setScrollSound(const std::shared_ptr<Sound>& sound) { mScrollSound = sound; }
|
||||
inline void setColor(unsigned int id, unsigned int color) { mColors[id] = color; }
|
||||
inline void setSound(const std::shared_ptr<Sound>& sound) { mScrollSound = sound; }
|
||||
inline void setLineSpacing(float lineSpacing) { mLineSpacing = lineSpacing; }
|
||||
|
||||
protected:
|
||||
virtual void onScroll(int amt) { if(mScrollSound) mScrollSound->play(); }
|
||||
virtual void onCursorChanged(const CursorState& state);
|
||||
|
||||
private:
|
||||
static const int MARQUEE_DELAY = 2000;
|
||||
static const int MARQUEE_SPEED = 8;
|
||||
static const int MARQUEE_RATE = 1;
|
||||
|
||||
int mMarqueeOffset;
|
||||
int mMarqueeTime;
|
||||
|
||||
Alignment mAlignment;
|
||||
float mHorizontalMargin;
|
||||
|
||||
std::function<void(CursorState state)> mCursorChangedCallback;
|
||||
|
||||
std::shared_ptr<Font> mFont;
|
||||
bool mUppercase;
|
||||
float mLineSpacing;
|
||||
unsigned int mSelectorColor;
|
||||
unsigned int mSelectedColor;
|
||||
std::shared_ptr<Sound> mScrollSound;
|
||||
static const unsigned int COLOR_ID_COUNT = 2;
|
||||
unsigned int mColors[COLOR_ID_COUNT];
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
TextListComponent<T>::TextListComponent(Window* window) :
|
||||
IList<TextListData, T>(window)
|
||||
{
|
||||
mMarqueeOffset = 0;
|
||||
mMarqueeTime = -MARQUEE_DELAY;
|
||||
|
||||
mHorizontalMargin = 0;
|
||||
mAlignment = ALIGN_CENTER;
|
||||
|
||||
mFont = Font::get(FONT_SIZE_MEDIUM);
|
||||
mUppercase = false;
|
||||
mLineSpacing = 1.5f;
|
||||
mSelectorColor = 0x000000FF;
|
||||
mSelectedColor = 0;
|
||||
mColors[0] = 0x0000FFFF;
|
||||
mColors[1] = 0x00FF00FF;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void TextListComponent<T>::render(const Eigen::Affine3f& parentTrans)
|
||||
{
|
||||
Eigen::Affine3f trans = parentTrans * getTransform();
|
||||
|
||||
std::shared_ptr<Font>& font = mFont;
|
||||
|
||||
if(size() == 0)
|
||||
return;
|
||||
|
||||
const float entrySize = round(font->getHeight(mLineSpacing));
|
||||
|
||||
int startEntry = 0;
|
||||
|
||||
//number of entries that can fit on the screen simultaniously
|
||||
int screenCount = (int)(mSize.y() / entrySize + 0.5f);
|
||||
|
||||
if(size() >= screenCount)
|
||||
{
|
||||
startEntry = mCursor - screenCount/2;
|
||||
if(startEntry < 0)
|
||||
startEntry = 0;
|
||||
if(startEntry >= size() - screenCount)
|
||||
startEntry = size() - screenCount;
|
||||
}
|
||||
|
||||
float y = 0;
|
||||
|
||||
int listCutoff = startEntry + screenCount;
|
||||
if(listCutoff > size())
|
||||
listCutoff = size();
|
||||
|
||||
// draw selector bar
|
||||
if(startEntry < listCutoff)
|
||||
{
|
||||
Renderer::setMatrix(trans);
|
||||
Renderer::drawRect(0.f, (mCursor - startEntry)*entrySize + (entrySize - font->getHeight())/2, mSize.x(), font->getHeight(), mSelectorColor);
|
||||
}
|
||||
|
||||
// clip to inside margins
|
||||
Eigen::Vector3f dim(mSize.x(), mSize.y(), 0);
|
||||
dim = trans * dim - trans.translation();
|
||||
Renderer::pushClipRect(Eigen::Vector2i((int)(trans.translation().x() + mHorizontalMargin), (int)trans.translation().y()),
|
||||
Eigen::Vector2i((int)(dim.x() - mHorizontalMargin*2), (int)dim.y()));
|
||||
|
||||
for(int i = startEntry; i < listCutoff; i++)
|
||||
{
|
||||
typename IList<TextListData, T>::Entry& entry = mEntries.at((unsigned int)i);
|
||||
|
||||
unsigned int color;
|
||||
if(mCursor == i && mSelectedColor)
|
||||
color = mSelectedColor;
|
||||
else
|
||||
color = mColors[entry.data.colorId];
|
||||
|
||||
if(!entry.data.textCache)
|
||||
entry.data.textCache = std::unique_ptr<TextCache>(font->buildTextCache(mUppercase ? strToUpper(entry.name) : entry.name, 0, 0, 0x000000FF));
|
||||
|
||||
entry.data.textCache->setColor(color);
|
||||
|
||||
Eigen::Vector3f offset(0, y, 0);
|
||||
|
||||
switch(mAlignment)
|
||||
{
|
||||
case ALIGN_LEFT:
|
||||
offset[0] = mHorizontalMargin;
|
||||
break;
|
||||
case ALIGN_CENTER:
|
||||
offset[0] = (mSize.x() - entry.data.textCache->metrics.size.x()) / 2;
|
||||
if(offset[0] < 0)
|
||||
offset[0] = 0;
|
||||
break;
|
||||
case ALIGN_RIGHT:
|
||||
offset[0] = (mSize.x() - entry.data.textCache->metrics.size.x());
|
||||
offset[0] -= mHorizontalMargin;
|
||||
if(offset[0] < 0)
|
||||
offset[0] = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if(mCursor == i)
|
||||
offset[0] -= mMarqueeOffset;
|
||||
|
||||
Eigen::Affine3f drawTrans = trans;
|
||||
drawTrans.translate(offset);
|
||||
Renderer::setMatrix(drawTrans);
|
||||
|
||||
font->renderTextCache(entry.data.textCache.get());
|
||||
|
||||
y += entrySize;
|
||||
}
|
||||
|
||||
Renderer::popClipRect();
|
||||
|
||||
listRenderTitleOverlay(trans);
|
||||
|
||||
GuiComponent::renderChildren(trans);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool TextListComponent<T>::input(InputConfig* config, Input input)
|
||||
{
|
||||
if(size() > 0)
|
||||
{
|
||||
if(input.value != 0)
|
||||
{
|
||||
if(config->isMappedTo("down", input))
|
||||
{
|
||||
listInput(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(config->isMappedTo("up", input))
|
||||
{
|
||||
listInput(-1);
|
||||
return true;
|
||||
}
|
||||
if(config->isMappedTo("pagedown", input))
|
||||
{
|
||||
listInput(10);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(config->isMappedTo("pageup", input))
|
||||
{
|
||||
listInput(-10);
|
||||
return true;
|
||||
}
|
||||
}else{
|
||||
if(config->isMappedTo("down", input) || config->isMappedTo("up", input) ||
|
||||
config->isMappedTo("pagedown", input) || config->isMappedTo("pageup", input))
|
||||
{
|
||||
stopScrolling();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return GuiComponent::input(config, input);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void TextListComponent<T>::update(int deltaTime)
|
||||
{
|
||||
listUpdate(deltaTime);
|
||||
if(!isScrolling() && size() > 0)
|
||||
{
|
||||
//if we're not scrolling and this object's text goes outside our size, marquee it!
|
||||
const std::string& text = mEntries.at((unsigned int)mCursor).name;
|
||||
|
||||
Eigen::Vector2f textSize = mFont->sizeText(text);
|
||||
|
||||
//it's long enough to marquee
|
||||
if(textSize.x() - mMarqueeOffset > mSize.x() - 12 - (mAlignment != ALIGN_CENTER ? mHorizontalMargin : 0))
|
||||
{
|
||||
mMarqueeTime += deltaTime;
|
||||
while(mMarqueeTime > MARQUEE_SPEED)
|
||||
{
|
||||
mMarqueeOffset += MARQUEE_RATE;
|
||||
mMarqueeTime -= MARQUEE_SPEED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GuiComponent::update(deltaTime);
|
||||
}
|
||||
|
||||
//list management stuff
|
||||
template <typename T>
|
||||
void TextListComponent<T>::add(const std::string& name, const T& obj, unsigned int color)
|
||||
{
|
||||
assert(color < COLOR_ID_COUNT);
|
||||
|
||||
typename IList<TextListData, T>::Entry entry;
|
||||
entry.name = name;
|
||||
entry.object = obj;
|
||||
entry.data.colorId = color;
|
||||
static_cast<IList< TextListData, T >*>(this)->add(entry);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void TextListComponent<T>::onCursorChanged(const CursorState& state)
|
||||
{
|
||||
mMarqueeOffset = 0;
|
||||
mMarqueeTime = -MARQUEE_DELAY;
|
||||
|
||||
if(mCursorChangedCallback)
|
||||
mCursorChangedCallback(state);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme, const std::string& view, const std::string& element, unsigned int properties)
|
||||
{
|
||||
GuiComponent::applyTheme(theme, view, element, properties);
|
||||
|
||||
const ThemeData::ThemeElement* elem = theme->getElement(view, element, "textlist");
|
||||
if(!elem)
|
||||
return;
|
||||
|
||||
using namespace ThemeFlags;
|
||||
if(properties & COLOR)
|
||||
{
|
||||
if(elem->has("selectorColor"))
|
||||
setSelectorColor(elem->get<unsigned int>("selectorColor"));
|
||||
if(elem->has("selectedColor"))
|
||||
setSelectedColor(elem->get<unsigned int>("selectedColor"));
|
||||
if(elem->has("primaryColor"))
|
||||
setColor(0, elem->get<unsigned int>("primaryColor"));
|
||||
if(elem->has("secondaryColor"))
|
||||
setColor(1, elem->get<unsigned int>("secondaryColor"));
|
||||
}
|
||||
|
||||
setFont(Font::getFromTheme(elem, properties, mFont));
|
||||
|
||||
if(properties & SOUND && elem->has("scrollSound"))
|
||||
setSound(Sound::get(elem->get<std::string>("scrollSound")));
|
||||
|
||||
if(properties & ALIGNMENT)
|
||||
{
|
||||
if(elem->has("alignment"))
|
||||
{
|
||||
const std::string& str = elem->get<std::string>("alignment");
|
||||
if(str == "left")
|
||||
setAlignment(ALIGN_LEFT);
|
||||
else if(str == "center")
|
||||
setAlignment(ALIGN_CENTER);
|
||||
else if(str == "right")
|
||||
setAlignment(ALIGN_RIGHT);
|
||||
else
|
||||
LOG(LogError) << "Unknown TextListComponent alignment \"" << str << "\"!";
|
||||
}
|
||||
if(elem->has("horizontalMargin"))
|
||||
{
|
||||
mHorizontalMargin = elem->get<float>("horizontalMargin") * (this->mParent ? this->mParent->getSize().x() : (float)Renderer::getScreenWidth());
|
||||
}
|
||||
}
|
||||
|
||||
if(properties & FORCE_UPPERCASE && elem->has("forceUppercase"))
|
||||
setUppercase(elem->get<bool>("forceUppercase"));
|
||||
|
||||
if(properties & LINE_SPACING && elem->has("lineSpacing"))
|
||||
setLineSpacing(elem->get<float>("lineSpacing"));
|
||||
}
|
163
es-app/src/guis/GuiFastSelect.cpp
Normal file
163
es-app/src/guis/GuiFastSelect.cpp
Normal file
|
@ -0,0 +1,163 @@
|
|||
#include "guis/GuiFastSelect.h"
|
||||
#include "ThemeData.h"
|
||||
#include "FileSorts.h"
|
||||
#include "SystemData.h"
|
||||
|
||||
static const std::string LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
GuiFastSelect::GuiFastSelect(Window* window, IGameListView* gamelist) : GuiComponent(window),
|
||||
mBackground(window), mSortText(window), mLetterText(window), mGameList(gamelist)
|
||||
{
|
||||
setPosition(Renderer::getScreenWidth() * 0.2f, Renderer::getScreenHeight() * 0.2f);
|
||||
setSize(Renderer::getScreenWidth() * 0.6f, Renderer::getScreenHeight() * 0.6f);
|
||||
|
||||
const std::shared_ptr<ThemeData>& theme = mGameList->getTheme();
|
||||
using namespace ThemeFlags;
|
||||
|
||||
mBackground.applyTheme(theme, "fastSelect", "windowBackground", PATH);
|
||||
mBackground.fitTo(mSize);
|
||||
addChild(&mBackground);
|
||||
|
||||
mLetterText.setSize(mSize.x(), mSize.y() * 0.75f);
|
||||
mLetterText.setAlignment(ALIGN_CENTER);
|
||||
mLetterText.applyTheme(theme, "fastSelect", "letter", FONT_PATH | COLOR);
|
||||
// TODO - set font size
|
||||
addChild(&mLetterText);
|
||||
|
||||
mSortText.setPosition(0, mSize.y() * 0.75f);
|
||||
mSortText.setSize(mSize.x(), mSize.y() * 0.25f);
|
||||
mSortText.setAlignment(ALIGN_CENTER);
|
||||
mSortText.applyTheme(theme, "fastSelect", "subtext", FONT_PATH | COLOR);
|
||||
// TODO - set font size
|
||||
addChild(&mSortText);
|
||||
|
||||
mSortId = 0; // TODO
|
||||
updateSortText();
|
||||
|
||||
mLetterId = LETTERS.find(mGameList->getCursor()->getName()[0]);
|
||||
if(mLetterId == std::string::npos)
|
||||
mLetterId = 0;
|
||||
|
||||
mScrollDir = 0;
|
||||
mScrollAccumulator = 0;
|
||||
scroll(); // initialize the letter value
|
||||
}
|
||||
|
||||
bool GuiFastSelect::input(InputConfig* config, Input input)
|
||||
{
|
||||
if(input.value == 0 && config->isMappedTo("select", input))
|
||||
{
|
||||
// the user let go of select; make our changes to the gamelist and close this gui
|
||||
updateGameListSort();
|
||||
updateGameListCursor();
|
||||
delete this;
|
||||
return true;
|
||||
}
|
||||
|
||||
if(config->isMappedTo("up", input))
|
||||
{
|
||||
if(input.value != 0)
|
||||
setScrollDir(-1);
|
||||
else
|
||||
setScrollDir(0);
|
||||
|
||||
return true;
|
||||
}else if(config->isMappedTo("down", input))
|
||||
{
|
||||
if(input.value != 0)
|
||||
setScrollDir(1);
|
||||
else
|
||||
setScrollDir(0);
|
||||
|
||||
return true;
|
||||
}else if(config->isMappedTo("left", input) && input.value != 0)
|
||||
{
|
||||
mSortId = (mSortId + 1) % FileSorts::SortTypes.size();
|
||||
updateSortText();
|
||||
return true;
|
||||
}else if(config->isMappedTo("right", input) && input.value != 0)
|
||||
{
|
||||
mSortId--;
|
||||
if(mSortId < 0)
|
||||
mSortId += FileSorts::SortTypes.size();
|
||||
|
||||
updateSortText();
|
||||
return true;
|
||||
}
|
||||
|
||||
return GuiComponent::input(config, input);
|
||||
}
|
||||
|
||||
void GuiFastSelect::setScrollDir(int dir)
|
||||
{
|
||||
mScrollDir = dir;
|
||||
scroll();
|
||||
mScrollAccumulator = -500;
|
||||
}
|
||||
|
||||
void GuiFastSelect::update(int deltaTime)
|
||||
{
|
||||
if(mScrollDir != 0)
|
||||
{
|
||||
mScrollAccumulator += deltaTime;
|
||||
while(mScrollAccumulator >= 150)
|
||||
{
|
||||
scroll();
|
||||
mScrollAccumulator -= 150;
|
||||
}
|
||||
}
|
||||
|
||||
GuiComponent::update(deltaTime);
|
||||
}
|
||||
|
||||
void GuiFastSelect::scroll()
|
||||
{
|
||||
mLetterId += mScrollDir;
|
||||
if(mLetterId < 0)
|
||||
mLetterId += LETTERS.length();
|
||||
else if(mLetterId >= (int)LETTERS.length())
|
||||
mLetterId -= LETTERS.length();
|
||||
|
||||
mLetterText.setText(LETTERS.substr(mLetterId, 1));
|
||||
}
|
||||
|
||||
void GuiFastSelect::updateSortText()
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "<- " << FileSorts::SortTypes.at(mSortId).description << " ->";
|
||||
mSortText.setText(ss.str());
|
||||
}
|
||||
|
||||
void GuiFastSelect::updateGameListSort()
|
||||
{
|
||||
const FileData::SortType& sort = FileSorts::SortTypes.at(mSortId);
|
||||
|
||||
FileData* root = mGameList->getCursor()->getSystem()->getRootFolder();
|
||||
root->sort(sort); // will also recursively sort children
|
||||
|
||||
// notify that the root folder was sorted
|
||||
mGameList->onFileChanged(root, FILE_SORTED);
|
||||
}
|
||||
|
||||
void GuiFastSelect::updateGameListCursor()
|
||||
{
|
||||
const std::vector<FileData*>& list = mGameList->getCursor()->getParent()->getChildren();
|
||||
|
||||
// only skip by letter when the sort mode is alphabetical
|
||||
const FileData::SortType& sort = FileSorts::SortTypes.at(mSortId);
|
||||
if(sort.comparisonFunction != &FileSorts::compareFileName)
|
||||
return;
|
||||
|
||||
// find the first entry in the list that either exactly matches our target letter or is beyond our target letter
|
||||
for(auto it = list.cbegin(); it != list.cend(); it++)
|
||||
{
|
||||
char check = (*it)->getName().empty() ? 'A' : (*it)->getName()[0];
|
||||
|
||||
// if there's an exact match or we've passed it, set the cursor to this one
|
||||
if(check == LETTERS[mLetterId] || (sort.ascending && check > LETTERS[mLetterId]) || (!sort.ascending && check < LETTERS[mLetterId]))
|
||||
{
|
||||
mGameList->setCursor(*it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
35
es-app/src/guis/GuiFastSelect.h
Normal file
35
es-app/src/guis/GuiFastSelect.h
Normal file
|
@ -0,0 +1,35 @@
|
|||
#pragma once
|
||||
|
||||
#include "GuiComponent.h"
|
||||
#include "views/gamelist/IGameListView.h"
|
||||
|
||||
#include "components/NinePatchComponent.h"
|
||||
#include "components/TextComponent.h"
|
||||
|
||||
class GuiFastSelect : public GuiComponent
|
||||
{
|
||||
public:
|
||||
GuiFastSelect(Window* window, IGameListView* gamelist);
|
||||
|
||||
bool input(InputConfig* config, Input input);
|
||||
void update(int deltaTime);
|
||||
|
||||
private:
|
||||
void setScrollDir(int dir);
|
||||
void scroll();
|
||||
void updateGameListCursor();
|
||||
void updateGameListSort();
|
||||
void updateSortText();
|
||||
|
||||
int mSortId;
|
||||
int mLetterId;
|
||||
|
||||
int mScrollDir;
|
||||
int mScrollAccumulator;
|
||||
|
||||
NinePatchComponent mBackground;
|
||||
TextComponent mSortText;
|
||||
TextComponent mLetterText;
|
||||
|
||||
IGameListView* mGameList;
|
||||
};
|
122
es-app/src/guis/GuiGameScraper.cpp
Normal file
122
es-app/src/guis/GuiGameScraper.cpp
Normal file
|
@ -0,0 +1,122 @@
|
|||
#include "guis/GuiGameScraper.h"
|
||||
#include "guis/GuiTextEditPopup.h"
|
||||
#include "components/TextComponent.h"
|
||||
#include "components/ButtonComponent.h"
|
||||
#include "components/MenuComponent.h"
|
||||
#include "scrapers/Scraper.h"
|
||||
#include "Renderer.h"
|
||||
#include "Log.h"
|
||||
#include "Settings.h"
|
||||
|
||||
GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std::function<void(const ScraperSearchResult&)> doneFunc) : GuiComponent(window),
|
||||
mGrid(window, Eigen::Vector2i(1, 7)),
|
||||
mBox(window, ":/frame.png"),
|
||||
mSearchParams(params),
|
||||
mClose(false)
|
||||
{
|
||||
addChild(&mBox);
|
||||
addChild(&mGrid);
|
||||
|
||||
// row 0 is a spacer
|
||||
|
||||
mGameName = std::make_shared<TextComponent>(mWindow, strToUpper(mSearchParams.game->getPath().filename().generic_string()),
|
||||
Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER);
|
||||
mGrid.setEntry(mGameName, Eigen::Vector2i(0, 1), false, true);
|
||||
|
||||
// row 2 is a spacer
|
||||
|
||||
mSystemName = std::make_shared<TextComponent>(mWindow, strToUpper(mSearchParams.system->getFullName()), Font::get(FONT_SIZE_SMALL),
|
||||
0x888888FF, ALIGN_CENTER);
|
||||
mGrid.setEntry(mSystemName, Eigen::Vector2i(0, 3), false, true);
|
||||
|
||||
// row 4 is a spacer
|
||||
|
||||
// ScraperSearchComponent
|
||||
mSearch = std::make_shared<ScraperSearchComponent>(window, ScraperSearchComponent::NEVER_AUTO_ACCEPT);
|
||||
mGrid.setEntry(mSearch, Eigen::Vector2i(0, 5), true);
|
||||
|
||||
// buttons
|
||||
std::vector< std::shared_ptr<ButtonComponent> > buttons;
|
||||
|
||||
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "INPUT", "search", [&] {
|
||||
mSearch->openInputScreen(mSearchParams);
|
||||
mGrid.resetCursor();
|
||||
}));
|
||||
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "CANCEL", "cancel", [&] { delete this; }));
|
||||
mButtonGrid = makeButtonGrid(mWindow, buttons);
|
||||
|
||||
mGrid.setEntry(mButtonGrid, Eigen::Vector2i(0, 6), true, false);
|
||||
|
||||
// we call this->close() instead of just delete this; in the accept callback:
|
||||
// this is because of how GuiComponent::update works. if it was just delete this, this would happen when the metadata resolver is done:
|
||||
// GuiGameScraper::update()
|
||||
// GuiComponent::update()
|
||||
// it = mChildren.begin();
|
||||
// mBox::update()
|
||||
// it++;
|
||||
// mSearchComponent::update()
|
||||
// acceptCallback -> delete this
|
||||
// it++; // error, mChildren has been deleted because it was part of this
|
||||
|
||||
// so instead we do this:
|
||||
// GuiGameScraper::update()
|
||||
// GuiComponent::update()
|
||||
// it = mChildren.begin();
|
||||
// mBox::update()
|
||||
// it++;
|
||||
// mSearchComponent::update()
|
||||
// acceptCallback -> close() -> mClose = true
|
||||
// it++; // ok
|
||||
// if(mClose)
|
||||
// delete this;
|
||||
mSearch->setAcceptCallback([this, doneFunc](const ScraperSearchResult& result) { doneFunc(result); close(); });
|
||||
mSearch->setCancelCallback([&] { delete this; });
|
||||
|
||||
setSize(Renderer::getScreenWidth() * 0.95f, Renderer::getScreenHeight() * 0.747f);
|
||||
setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2);
|
||||
|
||||
mGrid.resetCursor();
|
||||
mSearch->search(params); // start the search
|
||||
}
|
||||
|
||||
void GuiGameScraper::onSizeChanged()
|
||||
{
|
||||
mBox.fitTo(mSize, Eigen::Vector3f::Zero(), Eigen::Vector2f(-32, -32));
|
||||
|
||||
mGrid.setRowHeightPerc(0, 0.04f, false);
|
||||
mGrid.setRowHeightPerc(1, mGameName->getFont()->getLetterHeight() / mSize.y(), false); // game name
|
||||
mGrid.setRowHeightPerc(2, 0.04f, false);
|
||||
mGrid.setRowHeightPerc(3, mSystemName->getFont()->getLetterHeight() / mSize.y(), false); // system name
|
||||
mGrid.setRowHeightPerc(4, 0.04f, false);
|
||||
mGrid.setRowHeightPerc(6, mButtonGrid->getSize().y() / mSize.y(), false); // buttons
|
||||
mGrid.setSize(mSize);
|
||||
}
|
||||
|
||||
bool GuiGameScraper::input(InputConfig* config, Input input)
|
||||
{
|
||||
if(config->isMappedTo("b", input) && input.value)
|
||||
{
|
||||
delete this;
|
||||
return true;
|
||||
}
|
||||
|
||||
return GuiComponent::input(config, input);
|
||||
}
|
||||
|
||||
void GuiGameScraper::update(int deltaTime)
|
||||
{
|
||||
GuiComponent::update(deltaTime);
|
||||
|
||||
if(mClose)
|
||||
delete this;
|
||||
}
|
||||
|
||||
std::vector<HelpPrompt> GuiGameScraper::getHelpPrompts()
|
||||
{
|
||||
return mGrid.getHelpPrompts();
|
||||
}
|
||||
|
||||
void GuiGameScraper::close()
|
||||
{
|
||||
mClose = true;
|
||||
}
|
33
es-app/src/guis/GuiGameScraper.h
Normal file
33
es-app/src/guis/GuiGameScraper.h
Normal file
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include "GuiComponent.h"
|
||||
#include "components/ScraperSearchComponent.h"
|
||||
#include "components/NinePatchComponent.h"
|
||||
|
||||
class GuiGameScraper : public GuiComponent
|
||||
{
|
||||
public:
|
||||
GuiGameScraper(Window* window, ScraperSearchParams params, std::function<void(const ScraperSearchResult&)> doneFunc);
|
||||
|
||||
void onSizeChanged() override;
|
||||
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
void update(int deltaTime);
|
||||
virtual std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
|
||||
private:
|
||||
bool mClose;
|
||||
void close();
|
||||
|
||||
ComponentGrid mGrid;
|
||||
NinePatchComponent mBox;
|
||||
|
||||
std::shared_ptr<TextComponent> mGameName;
|
||||
std::shared_ptr<TextComponent> mSystemName;
|
||||
std::shared_ptr<ScraperSearchComponent> mSearch;
|
||||
std::shared_ptr<ComponentGrid> mButtonGrid;
|
||||
|
||||
ScraperSearchParams mSearchParams;
|
||||
|
||||
std::function<void()> mCancelFunc;
|
||||
};
|
81
es-app/src/guis/GuiGamelistOptions.cpp
Normal file
81
es-app/src/guis/GuiGamelistOptions.cpp
Normal file
|
@ -0,0 +1,81 @@
|
|||
#include "GuiGamelistOptions.h"
|
||||
#include "GuiMetaDataEd.h"
|
||||
#include "views/gamelist/IGameListView.h"
|
||||
#include "views/ViewController.h"
|
||||
|
||||
GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : GuiComponent(window),
|
||||
mSystem(system),
|
||||
mMenu(window, "OPTIONS")
|
||||
{
|
||||
addChild(&mMenu);
|
||||
|
||||
// sort list by
|
||||
mListSort = std::make_shared<SortList>(mWindow, "SORT GAMES BY", false);
|
||||
for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++)
|
||||
{
|
||||
const FileData::SortType& sort = FileSorts::SortTypes.at(i);
|
||||
mListSort->add(sort.description, &sort, i == 0); // TODO - actually make the sort type persistent
|
||||
}
|
||||
|
||||
mMenu.addWithLabel("SORT GAMES BY", mListSort);
|
||||
|
||||
// edit game metadata
|
||||
ComponentListRow row;
|
||||
row.addElement(std::make_shared<TextComponent>(mWindow, "EDIT THIS GAME'S METADATA", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true);
|
||||
row.addElement(makeArrow(mWindow), false);
|
||||
row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openMetaDataEd, this));
|
||||
mMenu.addRow(row);
|
||||
|
||||
// center the menu
|
||||
setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight());
|
||||
mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2);
|
||||
}
|
||||
|
||||
GuiGamelistOptions::~GuiGamelistOptions()
|
||||
{
|
||||
// apply sort
|
||||
FileData* root = getGamelist()->getCursor()->getSystem()->getRootFolder();
|
||||
root->sort(*mListSort->getSelected()); // will also recursively sort children
|
||||
|
||||
// notify that the root folder was sorted
|
||||
getGamelist()->onFileChanged(root, FILE_SORTED);
|
||||
}
|
||||
|
||||
void GuiGamelistOptions::openMetaDataEd()
|
||||
{
|
||||
// open metadata editor
|
||||
FileData* file = getGamelist()->getCursor();
|
||||
ScraperSearchParams p;
|
||||
p.game = file;
|
||||
p.system = file->getSystem();
|
||||
mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(), p, file->getPath().filename().string(),
|
||||
std::bind(&IGameListView::onFileChanged, getGamelist(), file, FILE_METADATA_CHANGED), [this, file] {
|
||||
boost::filesystem::remove(file->getPath()); //actually delete the file on the filesystem
|
||||
file->getParent()->removeChild(file); //unlink it so list repopulations triggered from onFileChanged won't see it
|
||||
getGamelist()->onFileChanged(file, FILE_REMOVED); //tell the view
|
||||
delete file; //free it
|
||||
}));
|
||||
}
|
||||
|
||||
bool GuiGamelistOptions::input(InputConfig* config, Input input)
|
||||
{
|
||||
if((config->isMappedTo("b", input) || config->isMappedTo("select", input)) && input.value)
|
||||
{
|
||||
delete this;
|
||||
return true;
|
||||
}
|
||||
|
||||
return mMenu.input(config, input);
|
||||
}
|
||||
|
||||
std::vector<HelpPrompt> GuiGamelistOptions::getHelpPrompts()
|
||||
{
|
||||
auto prompts = mMenu.getHelpPrompts();
|
||||
prompts.push_back(HelpPrompt("b", "close"));
|
||||
return prompts;
|
||||
}
|
||||
|
||||
IGameListView* GuiGamelistOptions::getGamelist()
|
||||
{
|
||||
return ViewController::get()->getGameListView(mSystem).get();
|
||||
}
|
27
es-app/src/guis/GuiGamelistOptions.h
Normal file
27
es-app/src/guis/GuiGamelistOptions.h
Normal file
|
@ -0,0 +1,27 @@
|
|||
#include "GuiComponent.h"
|
||||
#include "components/MenuComponent.h"
|
||||
#include "components/OptionListComponent.h"
|
||||
#include "FileSorts.h"
|
||||
|
||||
class IGameListView;
|
||||
|
||||
class GuiGamelistOptions : public GuiComponent
|
||||
{
|
||||
public:
|
||||
GuiGamelistOptions(Window* window, SystemData* system);
|
||||
virtual ~GuiGamelistOptions();
|
||||
|
||||
virtual bool input(InputConfig* config, Input input) override;
|
||||
virtual std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
|
||||
private:
|
||||
void openMetaDataEd();
|
||||
|
||||
MenuComponent mMenu;
|
||||
|
||||
typedef OptionListComponent<const FileData::SortType*> SortList;
|
||||
std::shared_ptr<SortList> mListSort;
|
||||
|
||||
SystemData* mSystem;
|
||||
IGameListView* getGamelist();
|
||||
};
|
277
es-app/src/guis/GuiMenu.cpp
Normal file
277
es-app/src/guis/GuiMenu.cpp
Normal file
|
@ -0,0 +1,277 @@
|
|||
#include "EmulationStation.h"
|
||||
#include "guis/GuiMenu.h"
|
||||
#include "Window.h"
|
||||
#include "Sound.h"
|
||||
#include "Log.h"
|
||||
#include "Settings.h"
|
||||
#include "guis/GuiMsgBox.h"
|
||||
#include "guis/GuiSettings.h"
|
||||
#include "guis/GuiScraperStart.h"
|
||||
#include "guis/GuiDetectDevice.h"
|
||||
#include "views/ViewController.h"
|
||||
|
||||
#include "components/ButtonComponent.h"
|
||||
#include "components/SwitchComponent.h"
|
||||
#include "components/SliderComponent.h"
|
||||
#include "components/TextComponent.h"
|
||||
#include "components/OptionListComponent.h"
|
||||
#include "components/MenuComponent.h"
|
||||
#include "VolumeControl.h"
|
||||
#include "scrapers/GamesDBScraper.h"
|
||||
#include "scrapers/TheArchiveScraper.h"
|
||||
|
||||
GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, "MAIN MENU"), mVersion(window)
|
||||
{
|
||||
// MAIN MENU
|
||||
|
||||
// SCRAPER >
|
||||
// SOUND SETTINGS >
|
||||
// UI SETTINGS >
|
||||
// CONFIGURE INPUT >
|
||||
// QUIT >
|
||||
|
||||
// [version]
|
||||
|
||||
auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); };
|
||||
addEntry("SCRAPER", 0x777777FF, true,
|
||||
[this, openScrapeNow] {
|
||||
auto s = new GuiSettings(mWindow, "SCRAPER");
|
||||
|
||||
// scrape from
|
||||
auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, "SCRAPE FROM", false);
|
||||
std::vector<std::string> scrapers = getScraperList();
|
||||
for(auto it = scrapers.begin(); it != scrapers.end(); it++)
|
||||
scraper_list->add(*it, *it, *it == Settings::getInstance()->getString("Scraper"));
|
||||
|
||||
s->addWithLabel("SCRAPE FROM", scraper_list);
|
||||
s->addSaveFunc([scraper_list] { Settings::getInstance()->setString("Scraper", scraper_list->getSelected()); });
|
||||
|
||||
// scrape ratings
|
||||
auto scrape_ratings = std::make_shared<SwitchComponent>(mWindow);
|
||||
scrape_ratings->setState(Settings::getInstance()->getBool("ScrapeRatings"));
|
||||
s->addWithLabel("SCRAPE RATINGS", scrape_ratings);
|
||||
s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); });
|
||||
|
||||
// scrape now
|
||||
ComponentListRow row;
|
||||
std::function<void()> openAndSave = openScrapeNow;
|
||||
openAndSave = [s, openAndSave] { s->save(); openAndSave(); };
|
||||
row.makeAcceptInputHandler(openAndSave);
|
||||
|
||||
auto scrape_now = std::make_shared<TextComponent>(mWindow, "SCRAPE NOW", Font::get(FONT_SIZE_MEDIUM), 0x777777FF);
|
||||
auto bracket = makeArrow(mWindow);
|
||||
row.addElement(scrape_now, true);
|
||||
row.addElement(bracket, false);
|
||||
s->addRow(row);
|
||||
|
||||
mWindow->pushGui(s);
|
||||
});
|
||||
|
||||
addEntry("SOUND SETTINGS", 0x777777FF, true,
|
||||
[this] {
|
||||
auto s = new GuiSettings(mWindow, "SOUND SETTINGS");
|
||||
|
||||
// volume
|
||||
auto volume = std::make_shared<SliderComponent>(mWindow, 0.f, 100.f, 1.f, "%");
|
||||
volume->setValue((float)VolumeControl::getInstance()->getVolume());
|
||||
s->addWithLabel("SYSTEM VOLUME", volume);
|
||||
s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)volume->getValue()); });
|
||||
|
||||
// disable sounds
|
||||
auto sounds_enabled = std::make_shared<SwitchComponent>(mWindow);
|
||||
sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds"));
|
||||
s->addWithLabel("ENABLE SOUNDS", sounds_enabled);
|
||||
s->addSaveFunc([sounds_enabled] { Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState()); });
|
||||
|
||||
mWindow->pushGui(s);
|
||||
});
|
||||
|
||||
addEntry("UI SETTINGS", 0x777777FF, true,
|
||||
[this] {
|
||||
auto s = new GuiSettings(mWindow, "UI SETTINGS");
|
||||
|
||||
// screensaver time
|
||||
auto screensaver_time = std::make_shared<SliderComponent>(mWindow, 0.f, 30.f, 1.f, "m");
|
||||
screensaver_time->setValue((float)(Settings::getInstance()->getInt("ScreenSaverTime") / (1000 * 60)));
|
||||
s->addWithLabel("SCREENSAVER AFTER", screensaver_time);
|
||||
s->addSaveFunc([screensaver_time] { Settings::getInstance()->setInt("ScreenSaverTime", (int)round(screensaver_time->getValue()) * (1000 * 60)); });
|
||||
|
||||
// screensaver behavior
|
||||
auto screensaver_behavior = std::make_shared< OptionListComponent<std::string> >(mWindow, "TRANSITION STYLE", false);
|
||||
std::vector<std::string> screensavers;
|
||||
screensavers.push_back("dim");
|
||||
screensavers.push_back("black");
|
||||
for(auto it = screensavers.begin(); it != screensavers.end(); it++)
|
||||
screensaver_behavior->add(*it, *it, Settings::getInstance()->getString("ScreenSaverBehavior") == *it);
|
||||
s->addWithLabel("SCREENSAVER BEHAVIOR", screensaver_behavior);
|
||||
s->addSaveFunc([screensaver_behavior] { Settings::getInstance()->setString("ScreenSaverBehavior", screensaver_behavior->getSelected()); });
|
||||
|
||||
// framerate
|
||||
auto framerate = std::make_shared<SwitchComponent>(mWindow);
|
||||
framerate->setState(Settings::getInstance()->getBool("DrawFramerate"));
|
||||
s->addWithLabel("SHOW FRAMERATE", framerate);
|
||||
s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); });
|
||||
|
||||
// show help
|
||||
auto show_help = std::make_shared<SwitchComponent>(mWindow);
|
||||
show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts"));
|
||||
s->addWithLabel("ON-SCREEN HELP", show_help);
|
||||
s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); });
|
||||
|
||||
// quick system select (left/right in game list view)
|
||||
auto quick_sys_select = std::make_shared<SwitchComponent>(mWindow);
|
||||
quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect"));
|
||||
s->addWithLabel("QUICK SYSTEM SELECT", quick_sys_select);
|
||||
s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); });
|
||||
|
||||
// transition style
|
||||
auto transition_style = std::make_shared< OptionListComponent<std::string> >(mWindow, "TRANSITION STYLE", false);
|
||||
std::vector<std::string> transitions;
|
||||
transitions.push_back("fade");
|
||||
transitions.push_back("slide");
|
||||
for(auto it = transitions.begin(); it != transitions.end(); it++)
|
||||
transition_style->add(*it, *it, Settings::getInstance()->getString("TransitionStyle") == *it);
|
||||
s->addWithLabel("TRANSITION STYLE", transition_style);
|
||||
s->addSaveFunc([transition_style] { Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); });
|
||||
|
||||
// theme set
|
||||
auto themeSets = ThemeData::getThemeSets();
|
||||
|
||||
if(!themeSets.empty())
|
||||
{
|
||||
auto selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet"));
|
||||
if(selectedSet == themeSets.end())
|
||||
selectedSet = themeSets.begin();
|
||||
|
||||
auto theme_set = std::make_shared< OptionListComponent<std::string> >(mWindow, "THEME SET", false);
|
||||
for(auto it = themeSets.begin(); it != themeSets.end(); it++)
|
||||
theme_set->add(it->first, it->first, it == selectedSet);
|
||||
s->addWithLabel("THEME SET", theme_set);
|
||||
|
||||
Window* window = mWindow;
|
||||
s->addSaveFunc([window, theme_set]
|
||||
{
|
||||
bool needReload = false;
|
||||
if(Settings::getInstance()->getString("ThemeSet") != theme_set->getSelected())
|
||||
needReload = true;
|
||||
|
||||
Settings::getInstance()->setString("ThemeSet", theme_set->getSelected());
|
||||
|
||||
if(needReload)
|
||||
ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation
|
||||
});
|
||||
}
|
||||
|
||||
mWindow->pushGui(s);
|
||||
});
|
||||
|
||||
addEntry("CONFIGURE INPUT", 0x777777FF, true,
|
||||
[this] {
|
||||
mWindow->pushGui(new GuiDetectDevice(mWindow, false, nullptr));
|
||||
});
|
||||
|
||||
addEntry("QUIT", 0x777777FF, true,
|
||||
[this] {
|
||||
auto s = new GuiSettings(mWindow, "QUIT");
|
||||
|
||||
Window* window = mWindow;
|
||||
|
||||
ComponentListRow row;
|
||||
row.makeAcceptInputHandler([window] {
|
||||
window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES",
|
||||
[] {
|
||||
if(system("sudo shutdown -r now") != 0)
|
||||
LOG(LogWarning) << "Restart terminated with non-zero result!";
|
||||
}, "NO", nullptr));
|
||||
});
|
||||
row.addElement(std::make_shared<TextComponent>(window, "RESTART SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true);
|
||||
s->addRow(row);
|
||||
|
||||
row.elements.clear();
|
||||
row.makeAcceptInputHandler([window] {
|
||||
window->pushGui(new GuiMsgBox(window, "REALLY SHUTDOWN?", "YES",
|
||||
[] {
|
||||
if(system("sudo shutdown -h now") != 0)
|
||||
LOG(LogWarning) << "Shutdown terminated with non-zero result!";
|
||||
}, "NO", nullptr));
|
||||
});
|
||||
row.addElement(std::make_shared<TextComponent>(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true);
|
||||
s->addRow(row);
|
||||
|
||||
if(Settings::getInstance()->getBool("ShowExit"))
|
||||
{
|
||||
row.elements.clear();
|
||||
row.makeAcceptInputHandler([window] {
|
||||
window->pushGui(new GuiMsgBox(window, "REALLY QUIT?", "YES",
|
||||
[] {
|
||||
SDL_Event ev;
|
||||
ev.type = SDL_QUIT;
|
||||
SDL_PushEvent(&ev);
|
||||
}, "NO", nullptr));
|
||||
});
|
||||
row.addElement(std::make_shared<TextComponent>(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true);
|
||||
s->addRow(row);
|
||||
}
|
||||
|
||||
mWindow->pushGui(s);
|
||||
});
|
||||
|
||||
mVersion.setFont(Font::get(FONT_SIZE_SMALL));
|
||||
mVersion.setColor(0xC6C6C6FF);
|
||||
mVersion.setText("EMULATIONSTATION V" + strToUpper(PROGRAM_VERSION_STRING));
|
||||
mVersion.setAlignment(ALIGN_CENTER);
|
||||
|
||||
addChild(&mMenu);
|
||||
addChild(&mVersion);
|
||||
|
||||
setSize(mMenu.getSize());
|
||||
setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, Renderer::getScreenHeight() * 0.15f);
|
||||
}
|
||||
|
||||
void GuiMenu::onSizeChanged()
|
||||
{
|
||||
mVersion.setSize(mSize.x(), 0);
|
||||
mVersion.setPosition(0, mSize.y() - mVersion.getSize().y());
|
||||
}
|
||||
|
||||
void GuiMenu::addEntry(const char* name, unsigned int color, bool add_arrow, const std::function<void()>& func)
|
||||
{
|
||||
std::shared_ptr<Font> font = Font::get(FONT_SIZE_MEDIUM);
|
||||
|
||||
// populate the list
|
||||
ComponentListRow row;
|
||||
row.addElement(std::make_shared<TextComponent>(mWindow, name, font, color), true);
|
||||
|
||||
if(add_arrow)
|
||||
{
|
||||
std::shared_ptr<ImageComponent> bracket = makeArrow(mWindow);
|
||||
row.addElement(bracket, false);
|
||||
}
|
||||
|
||||
row.makeAcceptInputHandler(func);
|
||||
|
||||
mMenu.addRow(row);
|
||||
}
|
||||
|
||||
bool GuiMenu::input(InputConfig* config, Input input)
|
||||
{
|
||||
if(GuiComponent::input(config, input))
|
||||
return true;
|
||||
|
||||
if((config->isMappedTo("b", input) || config->isMappedTo("start", input)) && input.value != 0)
|
||||
{
|
||||
delete this;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<HelpPrompt> GuiMenu::getHelpPrompts()
|
||||
{
|
||||
std::vector<HelpPrompt> prompts;
|
||||
prompts.push_back(HelpPrompt("up/down", "choose"));
|
||||
prompts.push_back(HelpPrompt("a", "select"));
|
||||
prompts.push_back(HelpPrompt("start", "close"));
|
||||
return prompts;
|
||||
}
|
21
es-app/src/guis/GuiMenu.h
Normal file
21
es-app/src/guis/GuiMenu.h
Normal file
|
@ -0,0 +1,21 @@
|
|||
#pragma once
|
||||
|
||||
#include "GuiComponent.h"
|
||||
#include "components/MenuComponent.h"
|
||||
#include <functional>
|
||||
|
||||
class GuiMenu : public GuiComponent
|
||||
{
|
||||
public:
|
||||
GuiMenu(Window* window);
|
||||
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
void onSizeChanged() override;
|
||||
std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
|
||||
private:
|
||||
void addEntry(const char* name, unsigned int color, bool add_arrow, const std::function<void()>& func);
|
||||
|
||||
MenuComponent mMenu;
|
||||
TextComponent mVersion;
|
||||
};
|
264
es-app/src/guis/GuiMetaDataEd.cpp
Normal file
264
es-app/src/guis/GuiMetaDataEd.cpp
Normal file
|
@ -0,0 +1,264 @@
|
|||
#include "guis/GuiMetaDataEd.h"
|
||||
#include "Renderer.h"
|
||||
#include "Log.h"
|
||||
#include "components/AsyncReqComponent.h"
|
||||
#include "Settings.h"
|
||||
#include "views/ViewController.h"
|
||||
#include "guis/GuiGameScraper.h"
|
||||
#include "guis/GuiMsgBox.h"
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include "components/TextEditComponent.h"
|
||||
#include "components/DateTimeComponent.h"
|
||||
#include "components/RatingComponent.h"
|
||||
#include "guis/GuiTextEditPopup.h"
|
||||
|
||||
using namespace Eigen;
|
||||
|
||||
GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector<MetaDataDecl>& mdd, ScraperSearchParams scraperParams,
|
||||
const std::string& header, std::function<void()> saveCallback, std::function<void()> deleteFunc) : GuiComponent(window),
|
||||
mScraperParams(scraperParams),
|
||||
|
||||
mBackground(window, ":/frame.png"),
|
||||
mGrid(window, Vector2i(1, 3)),
|
||||
|
||||
mMetaDataDecl(mdd),
|
||||
mMetaData(md),
|
||||
mSavedCallback(saveCallback), mDeleteFunc(deleteFunc)
|
||||
{
|
||||
addChild(&mBackground);
|
||||
addChild(&mGrid);
|
||||
|
||||
mHeaderGrid = std::make_shared<ComponentGrid>(mWindow, Vector2i(1, 5));
|
||||
|
||||
mTitle = std::make_shared<TextComponent>(mWindow, "EDIT METADATA", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER);
|
||||
mSubtitle = std::make_shared<TextComponent>(mWindow, strToUpper(scraperParams.game->getPath().filename().generic_string()),
|
||||
Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER);
|
||||
mHeaderGrid->setEntry(mTitle, Vector2i(0, 1), false, true);
|
||||
mHeaderGrid->setEntry(mSubtitle, Vector2i(0, 3), false, true);
|
||||
|
||||
mGrid.setEntry(mHeaderGrid, Vector2i(0, 0), false, true);
|
||||
|
||||
mList = std::make_shared<ComponentList>(mWindow);
|
||||
mGrid.setEntry(mList, Vector2i(0, 1), true, true);
|
||||
|
||||
// populate list
|
||||
for(auto iter = mdd.begin(); iter != mdd.end(); iter++)
|
||||
{
|
||||
std::shared_ptr<GuiComponent> ed;
|
||||
|
||||
// don't add statistics
|
||||
if(iter->isStatistic)
|
||||
continue;
|
||||
|
||||
// create ed and add it (and any related components) to mMenu
|
||||
// ed's value will be set below
|
||||
ComponentListRow row;
|
||||
auto lbl = std::make_shared<TextComponent>(mWindow, strToUpper(iter->displayName), Font::get(FONT_SIZE_SMALL), 0x777777FF);
|
||||
row.addElement(lbl, true); // label
|
||||
|
||||
switch(iter->type)
|
||||
{
|
||||
case MD_RATING:
|
||||
{
|
||||
ed = std::make_shared<RatingComponent>(window);
|
||||
const float height = lbl->getSize().y() * 0.71f;
|
||||
ed->setSize(0, height);
|
||||
row.addElement(ed, false, true);
|
||||
|
||||
auto spacer = std::make_shared<GuiComponent>(mWindow);
|
||||
spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0);
|
||||
row.addElement(spacer, false);
|
||||
|
||||
// pass input to the actual RatingComponent instead of the spacer
|
||||
row.input_handler = std::bind(&GuiComponent::input, ed.get(), std::placeholders::_1, std::placeholders::_2);
|
||||
|
||||
break;
|
||||
}
|
||||
case MD_DATE:
|
||||
{
|
||||
ed = std::make_shared<DateTimeComponent>(window);
|
||||
row.addElement(ed, false);
|
||||
|
||||
auto spacer = std::make_shared<GuiComponent>(mWindow);
|
||||
spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0);
|
||||
row.addElement(spacer, false);
|
||||
|
||||
// pass input to the actual DateTimeComponent instead of the spacer
|
||||
row.input_handler = std::bind(&GuiComponent::input, ed.get(), std::placeholders::_1, std::placeholders::_2);
|
||||
|
||||
break;
|
||||
}
|
||||
case MD_TIME:
|
||||
{
|
||||
ed = std::make_shared<DateTimeComponent>(window, DateTimeComponent::DISP_RELATIVE_TO_NOW);
|
||||
row.addElement(ed, false);
|
||||
break;
|
||||
}
|
||||
case MD_MULTILINE_STRING:
|
||||
default:
|
||||
{
|
||||
// MD_STRING
|
||||
ed = std::make_shared<TextComponent>(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT);
|
||||
row.addElement(ed, true);
|
||||
|
||||
auto spacer = std::make_shared<GuiComponent>(mWindow);
|
||||
spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0);
|
||||
row.addElement(spacer, false);
|
||||
|
||||
auto bracket = std::make_shared<ImageComponent>(mWindow);
|
||||
bracket->setImage(":/arrow.svg");
|
||||
bracket->setResize(Eigen::Vector2f(0, lbl->getFont()->getLetterHeight()));
|
||||
row.addElement(bracket, false);
|
||||
|
||||
bool multiLine = iter->type == MD_MULTILINE_STRING;
|
||||
const std::string title = iter->displayPrompt;
|
||||
auto updateVal = [ed](const std::string& newVal) { ed->setValue(newVal); }; // ok callback (apply new value to ed)
|
||||
row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] {
|
||||
mWindow->pushGui(new GuiTextEditPopup(mWindow, title, ed->getValue(), updateVal, multiLine));
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert(ed);
|
||||
mList->addRow(row);
|
||||
ed->setValue(mMetaData->get(iter->key));
|
||||
mEditors.push_back(ed);
|
||||
}
|
||||
|
||||
std::vector< std::shared_ptr<ButtonComponent> > buttons;
|
||||
|
||||
if(!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE))
|
||||
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "SCRAPE", "scrape", std::bind(&GuiMetaDataEd::fetch, this)));
|
||||
|
||||
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "SAVE", "save", [&] { save(); delete this; }));
|
||||
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "CANCEL", "cancel", [&] { delete this; }));
|
||||
|
||||
if(mDeleteFunc)
|
||||
{
|
||||
auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; };
|
||||
auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE A FILE!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); };
|
||||
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "DELETE", "delete", deleteBtnFunc));
|
||||
}
|
||||
|
||||
mButtons = makeButtonGrid(mWindow, buttons);
|
||||
mGrid.setEntry(mButtons, Vector2i(0, 2), true, false);
|
||||
|
||||
// resize + center
|
||||
setSize(Renderer::getScreenWidth() * 0.5f, Renderer::getScreenHeight() * 0.82f);
|
||||
setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2);
|
||||
}
|
||||
|
||||
void GuiMetaDataEd::onSizeChanged()
|
||||
{
|
||||
mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32));
|
||||
|
||||
mGrid.setSize(mSize);
|
||||
|
||||
const float titleHeight = mTitle->getFont()->getLetterHeight();
|
||||
const float subtitleHeight = mSubtitle->getFont()->getLetterHeight();
|
||||
const float titleSubtitleSpacing = mSize.y() * 0.03f;
|
||||
|
||||
mGrid.setRowHeightPerc(0, (titleHeight + titleSubtitleSpacing + subtitleHeight + TITLE_VERT_PADDING) / mSize.y());
|
||||
mGrid.setRowHeightPerc(2, mButtons->getSize().y() / mSize.y());
|
||||
|
||||
mHeaderGrid->setRowHeightPerc(1, titleHeight / mHeaderGrid->getSize().y());
|
||||
mHeaderGrid->setRowHeightPerc(2, titleSubtitleSpacing / mHeaderGrid->getSize().y());
|
||||
mHeaderGrid->setRowHeightPerc(3, subtitleHeight / mHeaderGrid->getSize().y());
|
||||
}
|
||||
|
||||
void GuiMetaDataEd::save()
|
||||
{
|
||||
for(unsigned int i = 0; i < mEditors.size(); i++)
|
||||
{
|
||||
if(mMetaDataDecl.at(i).isStatistic)
|
||||
continue;
|
||||
|
||||
mMetaData->set(mMetaDataDecl.at(i).key, mEditors.at(i)->getValue());
|
||||
}
|
||||
|
||||
if(mSavedCallback)
|
||||
mSavedCallback();
|
||||
}
|
||||
|
||||
void GuiMetaDataEd::fetch()
|
||||
{
|
||||
GuiGameScraper* scr = new GuiGameScraper(mWindow, mScraperParams, std::bind(&GuiMetaDataEd::fetchDone, this, std::placeholders::_1));
|
||||
mWindow->pushGui(scr);
|
||||
}
|
||||
|
||||
void GuiMetaDataEd::fetchDone(const ScraperSearchResult& result)
|
||||
{
|
||||
for(unsigned int i = 0; i < mEditors.size(); i++)
|
||||
{
|
||||
if(mMetaDataDecl.at(i).isStatistic)
|
||||
continue;
|
||||
|
||||
const std::string& key = mMetaDataDecl.at(i).key;
|
||||
mEditors.at(i)->setValue(result.mdl.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
void GuiMetaDataEd::close(bool closeAllWindows)
|
||||
{
|
||||
// find out if the user made any changes
|
||||
bool dirty = false;
|
||||
for(unsigned int i = 0; i < mEditors.size(); i++)
|
||||
{
|
||||
const std::string& key = mMetaDataDecl.at(i).key;
|
||||
if(mMetaData->get(key) != mEditors.at(i)->getValue())
|
||||
{
|
||||
dirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::function<void()> closeFunc;
|
||||
if(!closeAllWindows)
|
||||
{
|
||||
closeFunc = [this] { delete this; };
|
||||
}else{
|
||||
Window* window = mWindow;
|
||||
closeFunc = [window, this] {
|
||||
while(window->peekGui() != ViewController::get())
|
||||
delete window->peekGui();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if(dirty)
|
||||
{
|
||||
// changes were made, ask if the user wants to save them
|
||||
mWindow->pushGui(new GuiMsgBox(mWindow,
|
||||
"SAVE CHANGES?",
|
||||
"YES", [this, closeFunc] { save(); closeFunc(); },
|
||||
"NO", closeFunc
|
||||
));
|
||||
}else{
|
||||
closeFunc();
|
||||
}
|
||||
}
|
||||
|
||||
bool GuiMetaDataEd::input(InputConfig* config, Input input)
|
||||
{
|
||||
if(GuiComponent::input(config, input))
|
||||
return true;
|
||||
|
||||
const bool isStart = config->isMappedTo("start", input);
|
||||
if(input.value != 0 && (config->isMappedTo("b", input) || isStart))
|
||||
{
|
||||
close(isStart);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<HelpPrompt> GuiMetaDataEd::getHelpPrompts()
|
||||
{
|
||||
std::vector<HelpPrompt> prompts = mGrid.getHelpPrompts();
|
||||
prompts.push_back(HelpPrompt("b", "back"));
|
||||
prompts.push_back(HelpPrompt("start", "close"));
|
||||
return prompts;
|
||||
}
|
43
es-app/src/guis/GuiMetaDataEd.h
Normal file
43
es-app/src/guis/GuiMetaDataEd.h
Normal file
|
@ -0,0 +1,43 @@
|
|||
#pragma once
|
||||
|
||||
#include "GuiComponent.h"
|
||||
#include "components/MenuComponent.h"
|
||||
#include "MetaData.h"
|
||||
#include "scrapers/Scraper.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
class GuiMetaDataEd : public GuiComponent
|
||||
{
|
||||
public:
|
||||
GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector<MetaDataDecl>& mdd, ScraperSearchParams params,
|
||||
const std::string& header, std::function<void()> savedCallback, std::function<void()> deleteFunc);
|
||||
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
void onSizeChanged() override;
|
||||
virtual std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
|
||||
private:
|
||||
void save();
|
||||
void fetch();
|
||||
void fetchDone(const ScraperSearchResult& result);
|
||||
void close(bool closeAllWindows);
|
||||
|
||||
NinePatchComponent mBackground;
|
||||
ComponentGrid mGrid;
|
||||
|
||||
std::shared_ptr<TextComponent> mTitle;
|
||||
std::shared_ptr<TextComponent> mSubtitle;
|
||||
std::shared_ptr<ComponentGrid> mHeaderGrid;
|
||||
std::shared_ptr<ComponentList> mList;
|
||||
std::shared_ptr<ComponentGrid> mButtons;
|
||||
|
||||
ScraperSearchParams mScraperParams;
|
||||
|
||||
std::vector< std::shared_ptr<GuiComponent> > mEditors;
|
||||
|
||||
std::vector<MetaDataDecl> mMetaDataDecl;
|
||||
MetaDataList* mMetaData;
|
||||
std::function<void()> mSavedCallback;
|
||||
std::function<void()> mDeleteFunc;
|
||||
};
|
151
es-app/src/guis/GuiScraperMulti.cpp
Normal file
151
es-app/src/guis/GuiScraperMulti.cpp
Normal file
|
@ -0,0 +1,151 @@
|
|||
#include "guis/GuiScraperMulti.h"
|
||||
#include "Renderer.h"
|
||||
#include "Log.h"
|
||||
#include "views/ViewController.h"
|
||||
#include "Gamelist.h"
|
||||
|
||||
#include "components/TextComponent.h"
|
||||
#include "components/ButtonComponent.h"
|
||||
#include "components/ScraperSearchComponent.h"
|
||||
#include "components/MenuComponent.h" // for makeButtonGrid
|
||||
#include "guis/GuiMsgBox.h"
|
||||
|
||||
using namespace Eigen;
|
||||
|
||||
GuiScraperMulti::GuiScraperMulti(Window* window, const std::queue<ScraperSearchParams>& searches, bool approveResults) :
|
||||
GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 5)),
|
||||
mSearchQueue(searches)
|
||||
{
|
||||
assert(mSearchQueue.size());
|
||||
|
||||
addChild(&mBackground);
|
||||
addChild(&mGrid);
|
||||
|
||||
mTotalGames = mSearchQueue.size();
|
||||
mCurrentGame = 0;
|
||||
mTotalSuccessful = 0;
|
||||
mTotalSkipped = 0;
|
||||
|
||||
// set up grid
|
||||
mTitle = std::make_shared<TextComponent>(mWindow, "SCRAPING IN PROGRESS", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER);
|
||||
mGrid.setEntry(mTitle, Vector2i(0, 0), false, true);
|
||||
|
||||
mSystem = std::make_shared<TextComponent>(mWindow, "SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER);
|
||||
mGrid.setEntry(mSystem, Vector2i(0, 1), false, true);
|
||||
|
||||
mSubtitle = std::make_shared<TextComponent>(mWindow, "subtitle text", Font::get(FONT_SIZE_SMALL), 0x888888FF, ALIGN_CENTER);
|
||||
mGrid.setEntry(mSubtitle, Vector2i(0, 2), false, true);
|
||||
|
||||
mSearchComp = std::make_shared<ScraperSearchComponent>(mWindow,
|
||||
approveResults ? ScraperSearchComponent::ALWAYS_ACCEPT_MATCHING_CRC : ScraperSearchComponent::ALWAYS_ACCEPT_FIRST_RESULT);
|
||||
mSearchComp->setAcceptCallback(std::bind(&GuiScraperMulti::acceptResult, this, std::placeholders::_1));
|
||||
mSearchComp->setSkipCallback(std::bind(&GuiScraperMulti::skip, this));
|
||||
mSearchComp->setCancelCallback(std::bind(&GuiScraperMulti::finish, this));
|
||||
mGrid.setEntry(mSearchComp, Vector2i(0, 3), mSearchComp->getSearchType() != ScraperSearchComponent::ALWAYS_ACCEPT_FIRST_RESULT, true);
|
||||
|
||||
std::vector< std::shared_ptr<ButtonComponent> > buttons;
|
||||
|
||||
if(approveResults)
|
||||
{
|
||||
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "INPUT", "search", [&] {
|
||||
mSearchComp->openInputScreen(mSearchQueue.front());
|
||||
mGrid.resetCursor();
|
||||
}));
|
||||
|
||||
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "SKIP", "skip", [&] {
|
||||
skip();
|
||||
mGrid.resetCursor();
|
||||
}));
|
||||
}
|
||||
|
||||
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "STOP", "stop (progress saved)", std::bind(&GuiScraperMulti::finish, this)));
|
||||
|
||||
mButtonGrid = makeButtonGrid(mWindow, buttons);
|
||||
mGrid.setEntry(mButtonGrid, Vector2i(0, 4), true, false);
|
||||
|
||||
setSize(Renderer::getScreenWidth() * 0.95f, Renderer::getScreenHeight() * 0.849f);
|
||||
setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2);
|
||||
|
||||
doNextSearch();
|
||||
}
|
||||
|
||||
GuiScraperMulti::~GuiScraperMulti()
|
||||
{
|
||||
// view type probably changed (basic -> detailed)
|
||||
for(auto it = SystemData::sSystemVector.begin(); it != SystemData::sSystemVector.end(); it++)
|
||||
ViewController::get()->reloadGameListView(*it, false);
|
||||
}
|
||||
|
||||
void GuiScraperMulti::onSizeChanged()
|
||||
{
|
||||
mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32));
|
||||
|
||||
mGrid.setRowHeightPerc(0, mTitle->getFont()->getLetterHeight() * 1.9725f / mSize.y(), false);
|
||||
mGrid.setRowHeightPerc(1, (mSystem->getFont()->getLetterHeight() + 2) / mSize.y(), false);
|
||||
mGrid.setRowHeightPerc(2, mSubtitle->getFont()->getHeight() * 1.75f / mSize.y(), false);
|
||||
mGrid.setRowHeightPerc(4, mButtonGrid->getSize().y() / mSize.y(), false);
|
||||
mGrid.setSize(mSize);
|
||||
}
|
||||
|
||||
void GuiScraperMulti::doNextSearch()
|
||||
{
|
||||
if(mSearchQueue.empty())
|
||||
{
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// update title
|
||||
std::stringstream ss;
|
||||
mSystem->setText(strToUpper(mSearchQueue.front().system->getFullName()));
|
||||
|
||||
// update subtitle
|
||||
ss.str(""); // clear
|
||||
ss << "GAME " << (mCurrentGame + 1) << " OF " << mTotalGames << " - " << strToUpper(mSearchQueue.front().game->getPath().filename().string());
|
||||
mSubtitle->setText(ss.str());
|
||||
|
||||
mSearchComp->search(mSearchQueue.front());
|
||||
}
|
||||
|
||||
void GuiScraperMulti::acceptResult(const ScraperSearchResult& result)
|
||||
{
|
||||
ScraperSearchParams& search = mSearchQueue.front();
|
||||
|
||||
search.game->metadata = result.mdl;
|
||||
updateGamelist(search.system);
|
||||
|
||||
mSearchQueue.pop();
|
||||
mCurrentGame++;
|
||||
mTotalSuccessful++;
|
||||
doNextSearch();
|
||||
}
|
||||
|
||||
void GuiScraperMulti::skip()
|
||||
{
|
||||
mSearchQueue.pop();
|
||||
mCurrentGame++;
|
||||
mTotalSkipped++;
|
||||
doNextSearch();
|
||||
}
|
||||
|
||||
void GuiScraperMulti::finish()
|
||||
{
|
||||
std::stringstream ss;
|
||||
if(mTotalSuccessful == 0)
|
||||
{
|
||||
ss << "NO GAMES WERE SCRAPED.";
|
||||
}else{
|
||||
ss << mTotalSuccessful << " GAME" << ((mTotalSuccessful > 1) ? "S" : "") << " SUCCESSFULLY SCRAPED!";
|
||||
|
||||
if(mTotalSkipped > 0)
|
||||
ss << "\n" << mTotalSkipped << " GAME" << ((mTotalSkipped > 1) ? "S" : "") << " SKIPPED.";
|
||||
}
|
||||
|
||||
mWindow->pushGui(new GuiMsgBox(mWindow, ss.str(),
|
||||
"OK", [&] { delete this; }));
|
||||
}
|
||||
|
||||
std::vector<HelpPrompt> GuiScraperMulti::getHelpPrompts()
|
||||
{
|
||||
return mGrid.getHelpPrompts();
|
||||
}
|
43
es-app/src/guis/GuiScraperMulti.h
Normal file
43
es-app/src/guis/GuiScraperMulti.h
Normal file
|
@ -0,0 +1,43 @@
|
|||
#pragma once
|
||||
|
||||
#include "GuiComponent.h"
|
||||
#include "components/NinePatchComponent.h"
|
||||
#include "components/ComponentGrid.h"
|
||||
#include "scrapers/Scraper.h"
|
||||
|
||||
#include <queue>
|
||||
|
||||
class ScraperSearchComponent;
|
||||
class TextComponent;
|
||||
|
||||
class GuiScraperMulti : public GuiComponent
|
||||
{
|
||||
public:
|
||||
GuiScraperMulti(Window* window, const std::queue<ScraperSearchParams>& searches, bool approveResults);
|
||||
virtual ~GuiScraperMulti();
|
||||
|
||||
void onSizeChanged() override;
|
||||
std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
|
||||
private:
|
||||
void acceptResult(const ScraperSearchResult& result);
|
||||
void skip();
|
||||
void doNextSearch();
|
||||
|
||||
void finish();
|
||||
|
||||
unsigned int mTotalGames;
|
||||
unsigned int mCurrentGame;
|
||||
unsigned int mTotalSuccessful;
|
||||
unsigned int mTotalSkipped;
|
||||
std::queue<ScraperSearchParams> mSearchQueue;
|
||||
|
||||
NinePatchComponent mBackground;
|
||||
ComponentGrid mGrid;
|
||||
|
||||
std::shared_ptr<TextComponent> mTitle;
|
||||
std::shared_ptr<TextComponent> mSystem;
|
||||
std::shared_ptr<TextComponent> mSubtitle;
|
||||
std::shared_ptr<ScraperSearchComponent> mSearchComp;
|
||||
std::shared_ptr<ComponentGrid> mButtonGrid;
|
||||
};
|
127
es-app/src/guis/GuiScraperStart.cpp
Normal file
127
es-app/src/guis/GuiScraperStart.cpp
Normal file
|
@ -0,0 +1,127 @@
|
|||
#include "guis/GuiScraperStart.h"
|
||||
#include "guis/GuiScraperMulti.h"
|
||||
#include "guis/GuiMsgBox.h"
|
||||
#include "views/ViewController.h"
|
||||
|
||||
#include "components/TextComponent.h"
|
||||
#include "components/OptionListComponent.h"
|
||||
#include "components/SwitchComponent.h"
|
||||
|
||||
GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window),
|
||||
mMenu(window, "SCRAPE NOW")
|
||||
{
|
||||
addChild(&mMenu);
|
||||
|
||||
// add filters (with first one selected)
|
||||
mFilters = std::make_shared< OptionListComponent<GameFilterFunc> >(mWindow, "SCRAPE THESE GAMES", false);
|
||||
mFilters->add("All Games",
|
||||
[](SystemData*, FileData*) -> bool { return true; }, false);
|
||||
mFilters->add("Only missing image",
|
||||
[](SystemData*, FileData* g) -> bool { return g->metadata.get("image").empty(); }, true);
|
||||
mMenu.addWithLabel("Filter", mFilters);
|
||||
|
||||
//add systems (all with a platformid specified selected)
|
||||
mSystems = std::make_shared< OptionListComponent<SystemData*> >(mWindow, "SCRAPE THESE SYSTEMS", true);
|
||||
for(auto it = SystemData::sSystemVector.begin(); it != SystemData::sSystemVector.end(); it++)
|
||||
{
|
||||
if(!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE))
|
||||
mSystems->add((*it)->getFullName(), *it, !(*it)->getPlatformIds().empty());
|
||||
}
|
||||
mMenu.addWithLabel("Systems", mSystems);
|
||||
|
||||
mApproveResults = std::make_shared<SwitchComponent>(mWindow);
|
||||
mApproveResults->setState(true);
|
||||
mMenu.addWithLabel("User decides on conflicts", mApproveResults);
|
||||
|
||||
mMenu.addButton("START", "start", std::bind(&GuiScraperStart::pressedStart, this));
|
||||
mMenu.addButton("BACK", "back", [&] { delete this; });
|
||||
|
||||
mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f);
|
||||
}
|
||||
|
||||
void GuiScraperStart::pressedStart()
|
||||
{
|
||||
std::vector<SystemData*> sys = mSystems->getSelectedObjects();
|
||||
for(auto it = sys.begin(); it != sys.end(); it++)
|
||||
{
|
||||
if((*it)->getPlatformIds().empty())
|
||||
{
|
||||
mWindow->pushGui(new GuiMsgBox(mWindow,
|
||||
strToUpper("Warning: some of your selected systems do not have a platform set. Results may be even more inaccurate than usual!\nContinue anyway?"),
|
||||
"YES", std::bind(&GuiScraperStart::start, this),
|
||||
"NO", nullptr));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
start();
|
||||
}
|
||||
|
||||
void GuiScraperStart::start()
|
||||
{
|
||||
std::queue<ScraperSearchParams> searches = getSearches(mSystems->getSelectedObjects(), mFilters->getSelected());
|
||||
|
||||
if(searches.empty())
|
||||
{
|
||||
mWindow->pushGui(new GuiMsgBox(mWindow,
|
||||
"NO GAMES FIT THAT CRITERIA."));
|
||||
}else{
|
||||
GuiScraperMulti* gsm = new GuiScraperMulti(mWindow, searches, mApproveResults->getState());
|
||||
mWindow->pushGui(gsm);
|
||||
delete this;
|
||||
}
|
||||
}
|
||||
|
||||
std::queue<ScraperSearchParams> GuiScraperStart::getSearches(std::vector<SystemData*> systems, GameFilterFunc selector)
|
||||
{
|
||||
std::queue<ScraperSearchParams> queue;
|
||||
for(auto sys = systems.begin(); sys != systems.end(); sys++)
|
||||
{
|
||||
std::vector<FileData*> games = (*sys)->getRootFolder()->getFilesRecursive(GAME);
|
||||
for(auto game = games.begin(); game != games.end(); game++)
|
||||
{
|
||||
if(selector((*sys), (*game)))
|
||||
{
|
||||
ScraperSearchParams search;
|
||||
search.game = *game;
|
||||
search.system = *sys;
|
||||
|
||||
queue.push(search);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return queue;
|
||||
}
|
||||
|
||||
bool GuiScraperStart::input(InputConfig* config, Input input)
|
||||
{
|
||||
bool consumed = GuiComponent::input(config, input);
|
||||
if(consumed)
|
||||
return true;
|
||||
|
||||
if(input.value != 0 && config->isMappedTo("b", input))
|
||||
{
|
||||
delete this;
|
||||
return true;
|
||||
}
|
||||
|
||||
if(config->isMappedTo("start", input) && input.value != 0)
|
||||
{
|
||||
// close everything
|
||||
Window* window = mWindow;
|
||||
while(window->peekGui() && window->peekGui() != ViewController::get())
|
||||
delete window->peekGui();
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<HelpPrompt> GuiScraperStart::getHelpPrompts()
|
||||
{
|
||||
std::vector<HelpPrompt> prompts = mMenu.getHelpPrompts();
|
||||
prompts.push_back(HelpPrompt("b", "back"));
|
||||
prompts.push_back(HelpPrompt("start", "close"));
|
||||
return prompts;
|
||||
}
|
38
es-app/src/guis/GuiScraperStart.h
Normal file
38
es-app/src/guis/GuiScraperStart.h
Normal file
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
|
||||
#include "GuiComponent.h"
|
||||
#include "SystemData.h"
|
||||
#include "scrapers/Scraper.h"
|
||||
#include "components/MenuComponent.h"
|
||||
#include <queue>
|
||||
|
||||
typedef std::function<bool(SystemData*, FileData*)> GameFilterFunc;
|
||||
|
||||
template<typename T>
|
||||
class OptionListComponent;
|
||||
|
||||
class SwitchComponent;
|
||||
|
||||
//The starting point for a multi-game scrape.
|
||||
//Allows the user to set various parameters (to set filters, to set which systems to scrape, to enable manual mode).
|
||||
//Generates a list of "searches" that will be carried out by GuiScraperLog.
|
||||
class GuiScraperStart : public GuiComponent
|
||||
{
|
||||
public:
|
||||
GuiScraperStart(Window* window);
|
||||
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
|
||||
virtual std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
|
||||
private:
|
||||
void pressedStart();
|
||||
void start();
|
||||
std::queue<ScraperSearchParams> getSearches(std::vector<SystemData*> systems, GameFilterFunc selector);
|
||||
|
||||
std::shared_ptr< OptionListComponent<GameFilterFunc> > mFilters;
|
||||
std::shared_ptr< OptionListComponent<SystemData*> > mSystems;
|
||||
std::shared_ptr<SwitchComponent> mApproveResults;
|
||||
|
||||
MenuComponent mMenu;
|
||||
};
|
60
es-app/src/guis/GuiSettings.cpp
Normal file
60
es-app/src/guis/GuiSettings.cpp
Normal file
|
@ -0,0 +1,60 @@
|
|||
#include "guis/GuiSettings.h"
|
||||
#include "Window.h"
|
||||
#include "Settings.h"
|
||||
#include "views/ViewController.h"
|
||||
|
||||
GuiSettings::GuiSettings(Window* window, const char* title) : GuiComponent(window), mMenu(window, title)
|
||||
{
|
||||
addChild(&mMenu);
|
||||
|
||||
mMenu.addButton("BACK", "go back", [this] { delete this; });
|
||||
|
||||
setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight());
|
||||
mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f);
|
||||
}
|
||||
|
||||
GuiSettings::~GuiSettings()
|
||||
{
|
||||
save();
|
||||
}
|
||||
|
||||
void GuiSettings::save()
|
||||
{
|
||||
if(!mSaveFuncs.size())
|
||||
return;
|
||||
|
||||
for(auto it = mSaveFuncs.begin(); it != mSaveFuncs.end(); it++)
|
||||
(*it)();
|
||||
|
||||
Settings::getInstance()->saveFile();
|
||||
}
|
||||
|
||||
bool GuiSettings::input(InputConfig* config, Input input)
|
||||
{
|
||||
if(config->isMappedTo("b", input) && input.value != 0)
|
||||
{
|
||||
delete this;
|
||||
return true;
|
||||
}
|
||||
|
||||
if(config->isMappedTo("start", input) && input.value != 0)
|
||||
{
|
||||
// close everything
|
||||
Window* window = mWindow;
|
||||
while(window->peekGui() && window->peekGui() != ViewController::get())
|
||||
delete window->peekGui();
|
||||
return true;
|
||||
}
|
||||
|
||||
return GuiComponent::input(config, input);
|
||||
}
|
||||
|
||||
std::vector<HelpPrompt> GuiSettings::getHelpPrompts()
|
||||
{
|
||||
std::vector<HelpPrompt> prompts = mMenu.getHelpPrompts();
|
||||
|
||||
prompts.push_back(HelpPrompt("b", "back"));
|
||||
prompts.push_back(HelpPrompt("start", "close"));
|
||||
|
||||
return prompts;
|
||||
}
|
22
es-app/src/guis/GuiSettings.h
Normal file
22
es-app/src/guis/GuiSettings.h
Normal file
|
@ -0,0 +1,22 @@
|
|||
#include "GuiComponent.h"
|
||||
#include "components/MenuComponent.h"
|
||||
|
||||
// This is just a really simple template for a GUI that calls some save functions when closed.
|
||||
class GuiSettings : public GuiComponent
|
||||
{
|
||||
public:
|
||||
GuiSettings(Window* window, const char* title);
|
||||
virtual ~GuiSettings(); // just calls save();
|
||||
|
||||
void save();
|
||||
inline void addRow(const ComponentListRow& row) { mMenu.addRow(row); };
|
||||
inline void addWithLabel(const std::string& label, const std::shared_ptr<GuiComponent>& comp) { mMenu.addWithLabel(label, comp); };
|
||||
inline void addSaveFunc(const std::function<void()>& func) { mSaveFuncs.push_back(func); };
|
||||
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
|
||||
private:
|
||||
MenuComponent mMenu;
|
||||
std::vector< std::function<void()> > mSaveFuncs;
|
||||
};
|
280
es-app/src/main.cpp
Normal file
280
es-app/src/main.cpp
Normal file
|
@ -0,0 +1,280 @@
|
|||
//EmulationStation, a graphical front-end for ROM browsing. Created by Alec "Aloshi" Lofquist.
|
||||
//http://www.aloshi.com
|
||||
|
||||
#include <SDL.h>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include "Renderer.h"
|
||||
#include "views/ViewController.h"
|
||||
#include "SystemData.h"
|
||||
#include <boost/filesystem.hpp>
|
||||
#include "guis/GuiDetectDevice.h"
|
||||
#include "guis/GuiMsgBox.h"
|
||||
#include "AudioManager.h"
|
||||
#include "platform.h"
|
||||
#include "Log.h"
|
||||
#include "Window.h"
|
||||
#include "EmulationStation.h"
|
||||
#include "Settings.h"
|
||||
#include "ScraperCmdLine.h"
|
||||
#include <sstream>
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
bool scrape_cmdline = false;
|
||||
|
||||
bool parseArgs(int argc, char* argv[], unsigned int* width, unsigned int* height)
|
||||
{
|
||||
for(int i = 1; i < argc; i++)
|
||||
{
|
||||
if(strcmp(argv[i], "--resolution") == 0)
|
||||
{
|
||||
if(i >= argc - 2)
|
||||
{
|
||||
std::cerr << "Invalid resolution supplied.";
|
||||
return false;
|
||||
}
|
||||
|
||||
*width = atoi(argv[i + 1]);
|
||||
*height = atoi(argv[i + 2]);
|
||||
i += 2; // skip the argument value
|
||||
}else if(strcmp(argv[i], "--gamelist-only") == 0)
|
||||
{
|
||||
Settings::getInstance()->setBool("ParseGamelistOnly", true);
|
||||
}else if(strcmp(argv[i], "--ignore-gamelist") == 0)
|
||||
{
|
||||
Settings::getInstance()->setBool("IgnoreGamelist", true);
|
||||
}else if(strcmp(argv[i], "--draw-framerate") == 0)
|
||||
{
|
||||
Settings::getInstance()->setBool("DrawFramerate", true);
|
||||
}else if(strcmp(argv[i], "--no-exit") == 0)
|
||||
{
|
||||
Settings::getInstance()->setBool("ShowExit", false);
|
||||
}else if(strcmp(argv[i], "--debug") == 0)
|
||||
{
|
||||
Settings::getInstance()->setBool("Debug", true);
|
||||
Log::setReportingLevel(LogDebug);
|
||||
}else if(strcmp(argv[i], "--windowed") == 0)
|
||||
{
|
||||
Settings::getInstance()->setBool("Windowed", true);
|
||||
}else if(strcmp(argv[i], "--scrape") == 0)
|
||||
{
|
||||
scrape_cmdline = true;
|
||||
}else if(strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0)
|
||||
{
|
||||
std::cout <<
|
||||
"EmulationStation, a graphical front-end for ROM browsing.\n"
|
||||
"Written by Alec \"Aloshi\" Lofquist.\n"
|
||||
"Version " << PROGRAM_VERSION_STRING << ", built " << PROGRAM_BUILT_STRING << "\n\n"
|
||||
"Command line arguments:\n"
|
||||
"--resolution [width] [height] try and force a particular resolution\n"
|
||||
"--gamelist-only skip automatic game search, only read from gamelist.xml\n"
|
||||
"--ignore-gamelist ignore the gamelist (useful for troubleshooting)\n"
|
||||
"--draw-framerate display the framerate\n"
|
||||
"--no-exit don't show the exit option in the menu\n"
|
||||
"--debug even more logging\n"
|
||||
"--scrape scrape using command line interface\n"
|
||||
"--windowed not fullscreen, should be used with --resolution\n"
|
||||
"--help, -h summon a sentient, angry tuba\n\n"
|
||||
"More information available in README.md.\n";
|
||||
return false; //exit after printing help
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool verifyHomeFolderExists()
|
||||
{
|
||||
//make sure the config directory exists
|
||||
std::string home = getHomePath();
|
||||
std::string configDir = home + "/.emulationstation";
|
||||
if(!fs::exists(configDir))
|
||||
{
|
||||
std::cout << "Creating config directory \"" << configDir << "\"\n";
|
||||
fs::create_directory(configDir);
|
||||
if(!fs::exists(configDir))
|
||||
{
|
||||
std::cerr << "Config directory could not be created!\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns true if everything is OK,
|
||||
bool loadSystemConfigFile(const char** errorString)
|
||||
{
|
||||
*errorString = NULL;
|
||||
|
||||
if(!SystemData::loadConfig())
|
||||
{
|
||||
LOG(LogError) << "Error while parsing systems configuration file!";
|
||||
*errorString = "IT LOOKS LIKE YOUR SYSTEMS CONFIGURATION FILE HAS NOT BEEN SET UP OR IS INVALID. YOU'LL NEED TO DO THIS BY HAND, UNFORTUNATELY.\n\n"
|
||||
"VISIT EMULATIONSTATION.ORG FOR MORE INFORMATION.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(SystemData::sSystemVector.size() == 0)
|
||||
{
|
||||
LOG(LogError) << "No systems found! Does at least one system have a game present? (check that extensions match!)\n(Also, make sure you've updated your es_systems.cfg for XML!)";
|
||||
*errorString = "WE CAN'T FIND ANY SYSTEMS!\n"
|
||||
"CHECK THAT YOUR PATHS ARE CORRECT IN THE SYSTEMS CONFIGURATION FILE, "
|
||||
"AND YOUR GAME DIRECTORY HAS AT LEAST ONE GAME WITH THE CORRECT EXTENSION.\n\n"
|
||||
"VISIT EMULATIONSTATION.ORG FOR MORE INFORMATION.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//called on exit, assuming we get far enough to have the log initialized
|
||||
void onExit()
|
||||
{
|
||||
Log::close();
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
unsigned int width = 0;
|
||||
unsigned int height = 0;
|
||||
|
||||
if(!parseArgs(argc, argv, &width, &height))
|
||||
return 0;
|
||||
|
||||
//if ~/.emulationstation doesn't exist and cannot be created, bail
|
||||
if(!verifyHomeFolderExists())
|
||||
return 1;
|
||||
|
||||
//start the logger
|
||||
Log::open();
|
||||
LOG(LogInfo) << "EmulationStation - v" << PROGRAM_VERSION_STRING << ", built " << PROGRAM_BUILT_STRING;
|
||||
|
||||
//always close the log on exit
|
||||
atexit(&onExit);
|
||||
|
||||
Window window;
|
||||
ViewController::init(&window);
|
||||
window.pushGui(ViewController::get());
|
||||
|
||||
if(!scrape_cmdline)
|
||||
{
|
||||
if(!window.init(width, height))
|
||||
{
|
||||
LOG(LogError) << "Window failed to initialize!";
|
||||
return 1;
|
||||
}
|
||||
|
||||
window.renderLoadingScreen();
|
||||
}
|
||||
|
||||
const char* errorMsg = NULL;
|
||||
if(!loadSystemConfigFile(&errorMsg))
|
||||
{
|
||||
// something went terribly wrong
|
||||
if(errorMsg == NULL)
|
||||
{
|
||||
LOG(LogError) << "Unknown error occured while parsing system config file.";
|
||||
if(!scrape_cmdline)
|
||||
Renderer::deinit();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// we can't handle es_systems.cfg file problems inside ES itself, so display the error message then quit
|
||||
window.pushGui(new GuiMsgBox(&window,
|
||||
errorMsg,
|
||||
"QUIT", [] {
|
||||
SDL_Event* quit = new SDL_Event();
|
||||
quit->type = SDL_QUIT;
|
||||
SDL_PushEvent(quit);
|
||||
}));
|
||||
}
|
||||
|
||||
//run the command line scraper then quit
|
||||
if(scrape_cmdline)
|
||||
{
|
||||
return run_scraper_cmdline();
|
||||
}
|
||||
|
||||
//dont generate joystick events while we're loading (hopefully fixes "automatically started emulator" bug)
|
||||
SDL_JoystickEventState(SDL_DISABLE);
|
||||
|
||||
// preload what we can right away instead of waiting for the user to select it
|
||||
// this makes for no delays when accessing content, but a longer startup time
|
||||
ViewController::get()->preload();
|
||||
|
||||
//choose which GUI to open depending on if an input configuration already exists
|
||||
if(errorMsg == NULL)
|
||||
{
|
||||
if(fs::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0)
|
||||
{
|
||||
ViewController::get()->goToStart();
|
||||
}else{
|
||||
window.pushGui(new GuiDetectDevice(&window, true, [] { ViewController::get()->goToStart(); }));
|
||||
}
|
||||
}
|
||||
|
||||
//generate joystick events since we're done loading
|
||||
SDL_JoystickEventState(SDL_ENABLE);
|
||||
|
||||
int lastTime = SDL_GetTicks();
|
||||
bool running = true;
|
||||
|
||||
while(running)
|
||||
{
|
||||
SDL_Event event;
|
||||
while(SDL_PollEvent(&event))
|
||||
{
|
||||
switch(event.type)
|
||||
{
|
||||
case SDL_JOYHATMOTION:
|
||||
case SDL_JOYBUTTONDOWN:
|
||||
case SDL_JOYBUTTONUP:
|
||||
case SDL_KEYDOWN:
|
||||
case SDL_KEYUP:
|
||||
case SDL_JOYAXISMOTION:
|
||||
case SDL_TEXTINPUT:
|
||||
case SDL_TEXTEDITING:
|
||||
case SDL_JOYDEVICEADDED:
|
||||
case SDL_JOYDEVICEREMOVED:
|
||||
InputManager::getInstance()->parseEvent(event, &window);
|
||||
break;
|
||||
case SDL_QUIT:
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(window.isSleeping())
|
||||
{
|
||||
lastTime = SDL_GetTicks();
|
||||
SDL_Delay(1); // this doesn't need to be accurate, we're just giving up our CPU time until something wakes us up
|
||||
continue;
|
||||
}
|
||||
|
||||
int curTime = SDL_GetTicks();
|
||||
int deltaTime = curTime - lastTime;
|
||||
lastTime = curTime;
|
||||
|
||||
// cap deltaTime at 1000
|
||||
if(deltaTime > 1000 || deltaTime < 0)
|
||||
deltaTime = 1000;
|
||||
|
||||
window.update(deltaTime);
|
||||
window.render();
|
||||
Renderer::swapBuffers();
|
||||
|
||||
Log::flush();
|
||||
}
|
||||
|
||||
while(window.peekGui() != ViewController::get())
|
||||
delete window.peekGui();
|
||||
window.deinit();
|
||||
|
||||
SystemData::deleteSystems();
|
||||
|
||||
LOG(LogInfo) << "EmulationStation cleanly shutting down.";
|
||||
|
||||
return 0;
|
||||
}
|
163
es-app/src/scrapers/GamesDBScraper.cpp
Normal file
163
es-app/src/scrapers/GamesDBScraper.cpp
Normal file
|
@ -0,0 +1,163 @@
|
|||
#include "scrapers/GamesDBScraper.h"
|
||||
#include "Log.h"
|
||||
#include "pugixml/pugixml.hpp"
|
||||
#include "MetaData.h"
|
||||
#include "Settings.h"
|
||||
#include "Util.h"
|
||||
#include <boost/assign.hpp>
|
||||
|
||||
using namespace PlatformIds;
|
||||
const std::map<PlatformId, const char*> gamesdb_platformid_map = boost::assign::map_list_of
|
||||
(THREEDO, "3DO")
|
||||
(AMIGA, "Amiga")
|
||||
(AMSTRAD_CPC, "Amstrad CPC")
|
||||
// missing apple2
|
||||
(ARCADE, "Arcade")
|
||||
// missing atari 800
|
||||
(ATARI_2600, "Atari 2600")
|
||||
(ATARI_5200, "Atari 5200")
|
||||
(ATARI_7800, "Atari 7800")
|
||||
(ATARI_JAGUAR, "Atari Jaguar")
|
||||
(ATARI_JAGUAR_CD, "Atari Jaguar CD")
|
||||
(ATARI_LYNX, "Atari Lynx")
|
||||
// missing atari ST/STE/Falcon
|
||||
(ATARI_XE, "Atari XE")
|
||||
(COLECOVISION, "Colecovision")
|
||||
(COMMODORE_64, "Commodore 64")
|
||||
(INTELLIVISION, "Intellivision")
|
||||
(MAC_OS, "Mac OS")
|
||||
(XBOX, "Microsoft Xbox")
|
||||
(XBOX_360, "Microsoft Xbox 360")
|
||||
(NEOGEO, "NeoGeo")
|
||||
(NEOGEO_POCKET, "Neo Geo Pocket")
|
||||
(NEOGEO_POCKET_COLOR, "Neo Geo Pocket Color")
|
||||
(NINTENDO_3DS, "Nintendo 3DS")
|
||||
(NINTENDO_64, "Nintendo 64")
|
||||
(NINTENDO_DS, "Nintendo DS")
|
||||
(NINTENDO_ENTERTAINMENT_SYSTEM, "Nintendo Entertainment System (NES)")
|
||||
(GAME_BOY, "Nintendo Game Boy")
|
||||
(GAME_BOY_ADVANCE, "Nintendo Game Boy Advance")
|
||||
(GAME_BOY_COLOR, "Nintendo Game Boy Color")
|
||||
(NINTENDO_GAMECUBE, "Nintendo GameCube")
|
||||
(NINTENDO_WII, "Nintendo Wii")
|
||||
(NINTENDO_WII_U, "Nintendo Wii U")
|
||||
(PC, "PC")
|
||||
(SEGA_32X, "Sega 32X")
|
||||
(SEGA_CD, "Sega CD")
|
||||
(SEGA_DREAMCAST, "Sega Dreamcast")
|
||||
(SEGA_GAME_GEAR, "Sega Game Gear")
|
||||
(SEGA_GENESIS, "Sega Genesis")
|
||||
(SEGA_MASTER_SYSTEM, "Sega Master System")
|
||||
(SEGA_MEGA_DRIVE, "Sega Mega Drive")
|
||||
(SEGA_SATURN, "Sega Saturn")
|
||||
(PLAYSTATION, "Sony Playstation")
|
||||
(PLAYSTATION_2, "Sony Playstation 2")
|
||||
(PLAYSTATION_3, "Sony Playstation 3")
|
||||
(PLAYSTATION_4, "Sony Playstation 4")
|
||||
(PLAYSTATION_VITA, "Sony Playstation Vita")
|
||||
(PLAYSTATION_PORTABLE, "Sony PSP")
|
||||
(SUPER_NINTENDO, "Super Nintendo (SNES)")
|
||||
(TURBOGRAFX_16, "TurboGrafx 16")
|
||||
(WONDERSWAN, "WonderSwan")
|
||||
(WONDERSWAN_COLOR, "WonderSwan Color")
|
||||
(ZX_SPECTRUM, "Sinclair ZX Spectrum");
|
||||
|
||||
|
||||
void thegamesdb_generate_scraper_requests(const ScraperSearchParams& params, std::queue< std::unique_ptr<ScraperRequest> >& requests,
|
||||
std::vector<ScraperSearchResult>& results)
|
||||
{
|
||||
std::string path = "thegamesdb.net/api/GetGame.php?";
|
||||
|
||||
std::string cleanName = params.nameOverride;
|
||||
if(cleanName.empty())
|
||||
cleanName = params.game->getCleanName();
|
||||
|
||||
path += "name=" + HttpReq::urlEncode(cleanName);
|
||||
|
||||
if(params.system->getPlatformIds().empty())
|
||||
{
|
||||
// no platform specified, we're done
|
||||
requests.push(std::unique_ptr<ScraperRequest>(new TheGamesDBRequest(results, path)));
|
||||
}else{
|
||||
// go through the list, we need to split this into multiple requests
|
||||
// because TheGamesDB API either sucks or I don't know how to use it properly...
|
||||
std::string urlBase = path;
|
||||
auto& platforms = params.system->getPlatformIds();
|
||||
for(auto platformIt = platforms.begin(); platformIt != platforms.end(); platformIt++)
|
||||
{
|
||||
path = urlBase;
|
||||
auto mapIt = gamesdb_platformid_map.find(*platformIt);
|
||||
if(mapIt != gamesdb_platformid_map.end())
|
||||
{
|
||||
path += "&platform=";
|
||||
path += HttpReq::urlEncode(mapIt->second);
|
||||
}else{
|
||||
LOG(LogWarning) << "TheGamesDB scraper warning - no support for platform " << getPlatformName(*platformIt);
|
||||
}
|
||||
|
||||
requests.push(std::unique_ptr<ScraperRequest>(new TheGamesDBRequest(results, path)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TheGamesDBRequest::process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results)
|
||||
{
|
||||
assert(req->status() == HttpReq::REQ_SUCCESS);
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result parseResult = doc.load(req->getContent().c_str());
|
||||
if(!parseResult)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "GamesDBRequest - Error parsing XML. \n\t" << parseResult.description() << "";
|
||||
std::string err = ss.str();
|
||||
setError(err);
|
||||
LOG(LogError) << err;
|
||||
return;
|
||||
}
|
||||
|
||||
pugi::xml_node data = doc.child("Data");
|
||||
|
||||
std::string baseImageUrl = data.child("baseImgUrl").text().get();
|
||||
|
||||
pugi::xml_node game = data.child("Game");
|
||||
while(game && results.size() < MAX_SCRAPER_RESULTS)
|
||||
{
|
||||
ScraperSearchResult result;
|
||||
|
||||
result.mdl.set("name", game.child("GameTitle").text().get());
|
||||
result.mdl.set("desc", game.child("Overview").text().get());
|
||||
|
||||
boost::posix_time::ptime rd = string_to_ptime(game.child("ReleaseDate").text().get(), "%m/%d/%Y");
|
||||
result.mdl.setTime("releasedate", rd);
|
||||
|
||||
result.mdl.set("developer", game.child("Developer").text().get());
|
||||
result.mdl.set("publisher", game.child("Publisher").text().get());
|
||||
result.mdl.set("genre", game.child("Genres").first_child().text().get());
|
||||
result.mdl.set("players", game.child("Players").text().get());
|
||||
|
||||
if(Settings::getInstance()->getBool("ScrapeRatings") && game.child("Rating"))
|
||||
{
|
||||
float ratingVal = (game.child("Rating").text().as_int() / 10.0f);
|
||||
std::stringstream ss;
|
||||
ss << ratingVal;
|
||||
result.mdl.set("rating", ss.str());
|
||||
}
|
||||
|
||||
pugi::xml_node images = game.child("Images");
|
||||
|
||||
if(images)
|
||||
{
|
||||
pugi::xml_node art = images.find_child_by_attribute("boxart", "side", "front");
|
||||
|
||||
if(art)
|
||||
{
|
||||
result.thumbnailUrl = baseImageUrl + art.attribute("thumb").as_string();
|
||||
result.imageUrl = baseImageUrl + art.text().get();
|
||||
}
|
||||
}
|
||||
|
||||
results.push_back(result);
|
||||
game = game.next_sibling("Game");
|
||||
}
|
||||
}
|
14
es-app/src/scrapers/GamesDBScraper.h
Normal file
14
es-app/src/scrapers/GamesDBScraper.h
Normal file
|
@ -0,0 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include "scrapers/Scraper.h"
|
||||
|
||||
void thegamesdb_generate_scraper_requests(const ScraperSearchParams& params, std::queue< std::unique_ptr<ScraperRequest> >& requests,
|
||||
std::vector<ScraperSearchResult>& results);
|
||||
|
||||
class TheGamesDBRequest : public ScraperHttpRequest
|
||||
{
|
||||
public:
|
||||
TheGamesDBRequest(std::vector<ScraperSearchResult>& resultsWrite, const std::string& url) : ScraperHttpRequest(resultsWrite, url) {}
|
||||
protected:
|
||||
void process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results) override;
|
||||
};
|
293
es-app/src/scrapers/Scraper.cpp
Normal file
293
es-app/src/scrapers/Scraper.cpp
Normal file
|
@ -0,0 +1,293 @@
|
|||
#include "scrapers/Scraper.h"
|
||||
#include "Log.h"
|
||||
#include "Settings.h"
|
||||
#include <FreeImage.h>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/assign.hpp>
|
||||
|
||||
#include "GamesDBScraper.h"
|
||||
#include "TheArchiveScraper.h"
|
||||
|
||||
const std::map<std::string, generate_scraper_requests_func> scraper_request_funcs = boost::assign::map_list_of
|
||||
("TheGamesDB", &thegamesdb_generate_scraper_requests)
|
||||
("TheArchive", &thearchive_generate_scraper_requests);
|
||||
|
||||
std::unique_ptr<ScraperSearchHandle> startScraperSearch(const ScraperSearchParams& params)
|
||||
{
|
||||
const std::string& name = Settings::getInstance()->getString("Scraper");
|
||||
|
||||
std::unique_ptr<ScraperSearchHandle> handle(new ScraperSearchHandle());
|
||||
scraper_request_funcs.at(name)(params, handle->mRequestQueue, handle->mResults);
|
||||
return handle;
|
||||
}
|
||||
|
||||
std::vector<std::string> getScraperList()
|
||||
{
|
||||
std::vector<std::string> list;
|
||||
for(auto it = scraper_request_funcs.begin(); it != scraper_request_funcs.end(); it++)
|
||||
{
|
||||
list.push_back(it->first);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
// ScraperSearchHandle
|
||||
ScraperSearchHandle::ScraperSearchHandle()
|
||||
{
|
||||
setStatus(ASYNC_IN_PROGRESS);
|
||||
}
|
||||
|
||||
void ScraperSearchHandle::update()
|
||||
{
|
||||
if(mStatus == ASYNC_DONE)
|
||||
return;
|
||||
|
||||
while(!mRequestQueue.empty())
|
||||
{
|
||||
auto& req = mRequestQueue.front();
|
||||
AsyncHandleStatus status = req->status();
|
||||
|
||||
if(status == ASYNC_ERROR)
|
||||
{
|
||||
// propegate error
|
||||
setError(req->getStatusString());
|
||||
|
||||
// empty our queue
|
||||
while(!mRequestQueue.empty())
|
||||
mRequestQueue.pop();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// finished this one, see if we have any more
|
||||
if(status == ASYNC_DONE)
|
||||
{
|
||||
mRequestQueue.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
// status == ASYNC_IN_PROGRESS
|
||||
}
|
||||
|
||||
// we finished without any errors!
|
||||
if(mRequestQueue.empty())
|
||||
{
|
||||
setStatus(ASYNC_DONE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ScraperRequest
|
||||
ScraperRequest::ScraperRequest(std::vector<ScraperSearchResult>& resultsWrite) : mResults(resultsWrite)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
// ScraperHttpRequest
|
||||
ScraperHttpRequest::ScraperHttpRequest(std::vector<ScraperSearchResult>& resultsWrite, const std::string& url)
|
||||
: ScraperRequest(resultsWrite)
|
||||
{
|
||||
setStatus(ASYNC_IN_PROGRESS);
|
||||
mReq = std::unique_ptr<HttpReq>(new HttpReq(url));
|
||||
}
|
||||
|
||||
void ScraperHttpRequest::update()
|
||||
{
|
||||
HttpReq::Status status = mReq->status();
|
||||
if(status == HttpReq::REQ_SUCCESS)
|
||||
{
|
||||
setStatus(ASYNC_DONE); // if process() has an error, status will be changed to ASYNC_ERROR
|
||||
process(mReq, mResults);
|
||||
return;
|
||||
}
|
||||
|
||||
// not ready yet
|
||||
if(status == HttpReq::REQ_IN_PROGRESS)
|
||||
return;
|
||||
|
||||
// everything else is some sort of error
|
||||
LOG(LogError) << "ScraperHttpRequest network error (status: " << status << ") - " << mReq->getErrorMsg();
|
||||
setError(mReq->getErrorMsg());
|
||||
}
|
||||
|
||||
|
||||
// metadata resolving stuff
|
||||
|
||||
std::unique_ptr<MDResolveHandle> resolveMetaDataAssets(const ScraperSearchResult& result, const ScraperSearchParams& search)
|
||||
{
|
||||
return std::unique_ptr<MDResolveHandle>(new MDResolveHandle(result, search));
|
||||
}
|
||||
|
||||
MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search) : mResult(result)
|
||||
{
|
||||
if(!result.imageUrl.empty())
|
||||
{
|
||||
std::string imgPath = getSaveAsPath(search, "image", result.imageUrl);
|
||||
mFuncs.push_back(ResolvePair(downloadImageAsync(result.imageUrl, imgPath), [this, imgPath]
|
||||
{
|
||||
mResult.mdl.set("image", imgPath);
|
||||
mResult.imageUrl = "";
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void MDResolveHandle::update()
|
||||
{
|
||||
if(mStatus == ASYNC_DONE || mStatus == ASYNC_ERROR)
|
||||
return;
|
||||
|
||||
auto it = mFuncs.begin();
|
||||
while(it != mFuncs.end())
|
||||
{
|
||||
if(it->first->status() == ASYNC_ERROR)
|
||||
{
|
||||
setError(it->first->getStatusString());
|
||||
return;
|
||||
}else if(it->first->status() == ASYNC_DONE)
|
||||
{
|
||||
it->second();
|
||||
it = mFuncs.erase(it);
|
||||
continue;
|
||||
}
|
||||
it++;
|
||||
}
|
||||
|
||||
if(mFuncs.empty())
|
||||
setStatus(ASYNC_DONE);
|
||||
}
|
||||
|
||||
std::unique_ptr<ImageDownloadHandle> downloadImageAsync(const std::string& url, const std::string& saveAs)
|
||||
{
|
||||
return std::unique_ptr<ImageDownloadHandle>(new ImageDownloadHandle(url, saveAs,
|
||||
Settings::getInstance()->getInt("ScraperResizeWidth"), Settings::getInstance()->getInt("ScraperResizeHeight")));
|
||||
}
|
||||
|
||||
ImageDownloadHandle::ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight) :
|
||||
mSavePath(path), mMaxWidth(maxWidth), mMaxHeight(maxHeight), mReq(new HttpReq(url))
|
||||
{
|
||||
}
|
||||
|
||||
void ImageDownloadHandle::update()
|
||||
{
|
||||
if(mReq->status() == HttpReq::REQ_IN_PROGRESS)
|
||||
return;
|
||||
|
||||
if(mReq->status() != HttpReq::REQ_SUCCESS)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Network error: " << mReq->getErrorMsg();
|
||||
setError(ss.str());
|
||||
return;
|
||||
}
|
||||
|
||||
// download is done, save it to disk
|
||||
std::ofstream stream(mSavePath, std::ios_base::out | std::ios_base::binary);
|
||||
if(stream.bad())
|
||||
{
|
||||
setError("Failed to open image path to write. Permission error? Disk full?");
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string& content = mReq->getContent();
|
||||
stream.write(content.data(), content.length());
|
||||
stream.close();
|
||||
if(stream.bad())
|
||||
{
|
||||
setError("Failed to save image. Disk full?");
|
||||
return;
|
||||
}
|
||||
|
||||
// resize it
|
||||
if(!resizeImage(mSavePath, mMaxWidth, mMaxHeight))
|
||||
{
|
||||
setError("Error saving resized image. Out of memory? Disk full?");
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(ASYNC_DONE);
|
||||
}
|
||||
|
||||
//you can pass 0 for width or height to keep aspect ratio
|
||||
bool resizeImage(const std::string& path, int maxWidth, int maxHeight)
|
||||
{
|
||||
// nothing to do
|
||||
if(maxWidth == 0 && maxHeight == 0)
|
||||
return true;
|
||||
|
||||
FREE_IMAGE_FORMAT format = FIF_UNKNOWN;
|
||||
FIBITMAP* image = NULL;
|
||||
|
||||
//detect the filetype
|
||||
format = FreeImage_GetFileType(path.c_str(), 0);
|
||||
if(format == FIF_UNKNOWN)
|
||||
format = FreeImage_GetFIFFromFilename(path.c_str());
|
||||
if(format == FIF_UNKNOWN)
|
||||
{
|
||||
LOG(LogError) << "Error - could not detect filetype for image \"" << path << "\"!";
|
||||
return false;
|
||||
}
|
||||
|
||||
//make sure we can read this filetype first, then load it
|
||||
if(FreeImage_FIFSupportsReading(format))
|
||||
{
|
||||
image = FreeImage_Load(format, path.c_str());
|
||||
}else{
|
||||
LOG(LogError) << "Error - file format reading not supported for image \"" << path << "\"!";
|
||||
return false;
|
||||
}
|
||||
|
||||
float width = (float)FreeImage_GetWidth(image);
|
||||
float height = (float)FreeImage_GetHeight(image);
|
||||
|
||||
if(maxWidth == 0)
|
||||
{
|
||||
maxWidth = (int)((maxHeight / height) * width);
|
||||
}else if(maxHeight == 0)
|
||||
{
|
||||
maxHeight = (int)((maxWidth / width) * height);
|
||||
}
|
||||
|
||||
FIBITMAP* imageRescaled = FreeImage_Rescale(image, maxWidth, maxHeight, FILTER_BILINEAR);
|
||||
FreeImage_Unload(image);
|
||||
|
||||
if(imageRescaled == NULL)
|
||||
{
|
||||
LOG(LogError) << "Could not resize image! (not enough memory? invalid bitdepth?)";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool saved = FreeImage_Save(format, imageRescaled, path.c_str());
|
||||
FreeImage_Unload(imageRescaled);
|
||||
|
||||
if(!saved)
|
||||
LOG(LogError) << "Failed to save resized image!";
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix, const std::string& url)
|
||||
{
|
||||
const std::string subdirectory = params.system->getName();
|
||||
const std::string name = params.game->getPath().stem().generic_string() + "-" + suffix;
|
||||
|
||||
std::string path = getHomePath() + "/.emulationstation/downloaded_images/";
|
||||
|
||||
if(!boost::filesystem::exists(path))
|
||||
boost::filesystem::create_directory(path);
|
||||
|
||||
path += subdirectory + "/";
|
||||
|
||||
if(!boost::filesystem::exists(path))
|
||||
boost::filesystem::create_directory(path);
|
||||
|
||||
size_t dot = url.find_last_of('.');
|
||||
std::string ext;
|
||||
if(dot != std::string::npos)
|
||||
ext = url.substr(dot, std::string::npos);
|
||||
|
||||
path += name + ext;
|
||||
return path;
|
||||
}
|
156
es-app/src/scrapers/Scraper.h
Normal file
156
es-app/src/scrapers/Scraper.h
Normal file
|
@ -0,0 +1,156 @@
|
|||
#pragma once
|
||||
|
||||
#include "MetaData.h"
|
||||
#include "SystemData.h"
|
||||
#include "HttpReq.h"
|
||||
#include "AsyncHandle.h"
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <queue>
|
||||
|
||||
#define MAX_SCRAPER_RESULTS 7
|
||||
|
||||
struct ScraperSearchParams
|
||||
{
|
||||
SystemData* system;
|
||||
FileData* game;
|
||||
|
||||
std::string nameOverride;
|
||||
};
|
||||
|
||||
struct ScraperSearchResult
|
||||
{
|
||||
ScraperSearchResult() : mdl(GAME_METADATA) {};
|
||||
|
||||
MetaDataList mdl;
|
||||
std::string imageUrl;
|
||||
std::string thumbnailUrl;
|
||||
};
|
||||
|
||||
// So let me explain why I've abstracted this so heavily.
|
||||
// There are two ways I can think of that you'd want to write a scraper.
|
||||
|
||||
// 1. Do some HTTP request(s) -> process it -> return the results
|
||||
// 2. Do some local filesystem queries (an offline scraper) -> return the results
|
||||
|
||||
// The first way needs to be asynchronous while it's waiting for the HTTP request to return.
|
||||
// The second doesn't.
|
||||
|
||||
// It would be nice if we could write it like this:
|
||||
// search = generate_http_request(searchparams);
|
||||
// wait_until_done(search);
|
||||
// ... process search ...
|
||||
// return results;
|
||||
|
||||
// We could do this if we used threads. Right now ES doesn't because I'm pretty sure I'll fuck it up,
|
||||
// and I'm not sure of the performance of threads on the Pi (single-core ARM).
|
||||
// We could also do this if we used coroutines.
|
||||
// I can't find a really good cross-platform coroutine library (x86/64/ARM Linux + Windows),
|
||||
// and I don't want to spend more time chasing libraries than just writing it the long way once.
|
||||
|
||||
// So, I did it the "long" way.
|
||||
// ScraperSearchHandle - one logical search, e.g. "search for mario"
|
||||
// ScraperRequest - encapsulates some sort of asynchronous request that will ultimately return some results
|
||||
// ScraperHttpRequest - implementation of ScraperRequest that waits on an HttpReq, then processes it with some processing function.
|
||||
|
||||
|
||||
// a scraper search gathers results from (potentially multiple) ScraperRequests
|
||||
class ScraperRequest : public AsyncHandle
|
||||
{
|
||||
public:
|
||||
ScraperRequest(std::vector<ScraperSearchResult>& resultsWrite);
|
||||
|
||||
// returns "true" once we're done
|
||||
virtual void update() = 0;
|
||||
|
||||
protected:
|
||||
std::vector<ScraperSearchResult>& mResults;
|
||||
};
|
||||
|
||||
|
||||
// a single HTTP request that needs to be processed to get the results
|
||||
class ScraperHttpRequest : public ScraperRequest
|
||||
{
|
||||
public:
|
||||
ScraperHttpRequest(std::vector<ScraperSearchResult>& resultsWrite, const std::string& url);
|
||||
virtual void update() override;
|
||||
|
||||
protected:
|
||||
virtual void process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results) = 0;
|
||||
|
||||
private:
|
||||
std::unique_ptr<HttpReq> mReq;
|
||||
};
|
||||
|
||||
// a request to get a list of results
|
||||
class ScraperSearchHandle : public AsyncHandle
|
||||
{
|
||||
public:
|
||||
ScraperSearchHandle();
|
||||
|
||||
void update();
|
||||
inline const std::vector<ScraperSearchResult>& getResults() const { assert(mStatus != ASYNC_IN_PROGRESS); return mResults; }
|
||||
|
||||
protected:
|
||||
friend std::unique_ptr<ScraperSearchHandle> startScraperSearch(const ScraperSearchParams& params);
|
||||
|
||||
std::queue< std::unique_ptr<ScraperRequest> > mRequestQueue;
|
||||
std::vector<ScraperSearchResult> mResults;
|
||||
};
|
||||
|
||||
// will use the current scraper settings to pick the result source
|
||||
std::unique_ptr<ScraperSearchHandle> startScraperSearch(const ScraperSearchParams& params);
|
||||
|
||||
// returns a list of valid scraper names
|
||||
std::vector<std::string> getScraperList();
|
||||
|
||||
typedef void (*generate_scraper_requests_func)(const ScraperSearchParams& params, std::queue< std::unique_ptr<ScraperRequest> >& requests, std::vector<ScraperSearchResult>& results);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
// Meta data asset downloading stuff.
|
||||
class MDResolveHandle : public AsyncHandle
|
||||
{
|
||||
public:
|
||||
MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search);
|
||||
|
||||
void update() override;
|
||||
inline const ScraperSearchResult& getResult() const { assert(mStatus == ASYNC_DONE); return mResult; }
|
||||
|
||||
private:
|
||||
ScraperSearchResult mResult;
|
||||
|
||||
typedef std::pair< std::unique_ptr<AsyncHandle>, std::function<void()> > ResolvePair;
|
||||
std::vector<ResolvePair> mFuncs;
|
||||
};
|
||||
|
||||
class ImageDownloadHandle : public AsyncHandle
|
||||
{
|
||||
public:
|
||||
ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight);
|
||||
|
||||
void update() override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<HttpReq> mReq;
|
||||
std::string mSavePath;
|
||||
int mMaxWidth;
|
||||
int mMaxHeight;
|
||||
};
|
||||
|
||||
//About the same as "~/.emulationstation/downloaded_images/[system_name]/[game_name].[url's extension]".
|
||||
//Will create the "downloaded_images" and "subdirectory" directories if they do not exist.
|
||||
std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix, const std::string& url);
|
||||
|
||||
//Will resize according to Settings::getInt("ScraperResizeWidth") and Settings::getInt("ScraperResizeHeight").
|
||||
std::unique_ptr<ImageDownloadHandle> downloadImageAsync(const std::string& url, const std::string& saveAs);
|
||||
|
||||
// Resolves all metadata assets that need to be downloaded.
|
||||
std::unique_ptr<MDResolveHandle> resolveMetaDataAssets(const ScraperSearchResult& result, const ScraperSearchParams& search);
|
||||
|
||||
//You can pass 0 for maxWidth or maxHeight to automatically keep the aspect ratio.
|
||||
//Will overwrite the image at [path] with the new resized one.
|
||||
//Returns true if successful, false otherwise.
|
||||
bool resizeImage(const std::string& path, int maxWidth, int maxHeight);
|
66
es-app/src/scrapers/TheArchiveScraper.cpp
Normal file
66
es-app/src/scrapers/TheArchiveScraper.cpp
Normal file
|
@ -0,0 +1,66 @@
|
|||
#include "TheArchiveScraper.h"
|
||||
#include "Log.h"
|
||||
#include "pugixml/pugixml.hpp"
|
||||
|
||||
void thearchive_generate_scraper_requests(const ScraperSearchParams& params, std::queue< std::unique_ptr<ScraperRequest> >& requests,
|
||||
std::vector<ScraperSearchResult>& results)
|
||||
{
|
||||
std::string path = "api.archive.vg/2.0/Archive.search/xml/7TTRM4MNTIKR2NNAGASURHJOZJ3QXQC5/";
|
||||
|
||||
std::string cleanName = params.nameOverride;
|
||||
if(cleanName.empty())
|
||||
cleanName = params.game->getCleanName();
|
||||
|
||||
path += HttpReq::urlEncode(cleanName);
|
||||
//platform TODO, should use some params.system get method
|
||||
|
||||
requests.push(std::unique_ptr<ScraperRequest>(new TheArchiveRequest(results, path)));
|
||||
}
|
||||
|
||||
void TheArchiveRequest::process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results)
|
||||
{
|
||||
assert(req->status() == HttpReq::REQ_SUCCESS);
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result parseResult = doc.load(req->getContent().c_str());
|
||||
if(!parseResult)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "TheArchiveRequest - error parsing XML.\n\t" << parseResult.description();
|
||||
std::string err = ss.str();
|
||||
setError(err);
|
||||
LOG(LogError) << err;
|
||||
return;
|
||||
}
|
||||
|
||||
pugi::xml_node data = doc.child("OpenSearchDescription").child("games");
|
||||
|
||||
pugi::xml_node game = data.child("game");
|
||||
while(game && results.size() < MAX_SCRAPER_RESULTS)
|
||||
{
|
||||
ScraperSearchResult result;
|
||||
|
||||
result.mdl.set("name", game.child("title").text().get());
|
||||
result.mdl.set("desc", game.child("description").text().get());
|
||||
|
||||
//Archive.search does not return ratings
|
||||
|
||||
result.mdl.set("developer", game.child("developer").text().get());
|
||||
|
||||
std::string genre = game.child("genre").text().get();
|
||||
size_t search = genre.find_last_of(" > ");
|
||||
genre = genre.substr(search == std::string::npos ? 0 : search, std::string::npos);
|
||||
result.mdl.set("genre", genre);
|
||||
|
||||
pugi::xml_node image = game.child("box_front");
|
||||
pugi::xml_node thumbnail = game.child("box_front_small");
|
||||
|
||||
if(image)
|
||||
result.imageUrl = image.text().get();
|
||||
if(thumbnail)
|
||||
result.thumbnailUrl = thumbnail.text().get();
|
||||
|
||||
results.push_back(result);
|
||||
game = game.next_sibling("game");
|
||||
}
|
||||
}
|
16
es-app/src/scrapers/TheArchiveScraper.h
Normal file
16
es-app/src/scrapers/TheArchiveScraper.h
Normal file
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include "scrapers/Scraper.h"
|
||||
|
||||
void thearchive_generate_scraper_requests(const ScraperSearchParams& params, std::queue< std::unique_ptr<ScraperRequest> >& requests,
|
||||
std::vector<ScraperSearchResult>& results);
|
||||
|
||||
void thearchive_process_httpreq(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results);
|
||||
|
||||
class TheArchiveRequest : public ScraperHttpRequest
|
||||
{
|
||||
public:
|
||||
TheArchiveRequest(std::vector<ScraperSearchResult>& resultsWrite, const std::string& url) : ScraperHttpRequest(resultsWrite, url) {}
|
||||
protected:
|
||||
void process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results) override;
|
||||
};
|
345
es-app/src/views/SystemView.cpp
Normal file
345
es-app/src/views/SystemView.cpp
Normal file
|
@ -0,0 +1,345 @@
|
|||
#include "views/SystemView.h"
|
||||
#include "SystemData.h"
|
||||
#include "Renderer.h"
|
||||
#include "Log.h"
|
||||
#include "Window.h"
|
||||
#include "views/ViewController.h"
|
||||
#include "animations/LambdaAnimation.h"
|
||||
#include "SystemData.h"
|
||||
#include "Settings.h"
|
||||
#include "Util.h"
|
||||
|
||||
#define SELECTED_SCALE 1.5f
|
||||
#define LOGO_PADDING ((logoSize().x() * (SELECTED_SCALE - 1)/2) + (mSize.x() * 0.06f))
|
||||
#define BAND_HEIGHT (logoSize().y() * SELECTED_SCALE)
|
||||
|
||||
SystemView::SystemView(Window* window) : IList<SystemViewData, SystemData*>(window, LIST_SCROLL_STYLE_SLOW, LIST_ALWAYS_LOOP),
|
||||
mSystemInfo(window, "SYSTEM INFO", Font::get(FONT_SIZE_SMALL), 0x33333300, ALIGN_CENTER)
|
||||
{
|
||||
mCamOffset = 0;
|
||||
mExtrasCamOffset = 0;
|
||||
mExtrasFadeOpacity = 0.0f;
|
||||
|
||||
setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight());
|
||||
|
||||
mSystemInfo.setSize(mSize.x(), mSystemInfo.getSize().y() * 1.333f);
|
||||
mSystemInfo.setPosition(0, (mSize.y() + BAND_HEIGHT) / 2);
|
||||
|
||||
populate();
|
||||
}
|
||||
|
||||
void SystemView::populate()
|
||||
{
|
||||
mEntries.clear();
|
||||
|
||||
for(auto it = SystemData::sSystemVector.begin(); it != SystemData::sSystemVector.end(); it++)
|
||||
{
|
||||
const std::shared_ptr<ThemeData>& theme = (*it)->getTheme();
|
||||
|
||||
Entry e;
|
||||
e.name = (*it)->getName();
|
||||
e.object = *it;
|
||||
|
||||
// make logo
|
||||
if(theme->getElement("system", "logo", "image"))
|
||||
{
|
||||
ImageComponent* logo = new ImageComponent(mWindow);
|
||||
logo->setMaxSize(Eigen::Vector2f(logoSize().x(), logoSize().y()));
|
||||
logo->applyTheme((*it)->getTheme(), "system", "logo", ThemeFlags::PATH);
|
||||
logo->setPosition((logoSize().x() - logo->getSize().x()) / 2, (logoSize().y() - logo->getSize().y()) / 2); // center
|
||||
e.data.logo = std::shared_ptr<GuiComponent>(logo);
|
||||
|
||||
ImageComponent* logoSelected = new ImageComponent(mWindow);
|
||||
logoSelected->setMaxSize(Eigen::Vector2f(logoSize().x() * SELECTED_SCALE, logoSize().y() * SELECTED_SCALE * 0.70f));
|
||||
logoSelected->applyTheme((*it)->getTheme(), "system", "logo", ThemeFlags::PATH);
|
||||
logoSelected->setPosition((logoSize().x() - logoSelected->getSize().x()) / 2,
|
||||
(logoSize().y() - logoSelected->getSize().y()) / 2); // center
|
||||
e.data.logoSelected = std::shared_ptr<GuiComponent>(logoSelected);
|
||||
}else{
|
||||
// no logo in theme; use text
|
||||
TextComponent* text = new TextComponent(mWindow,
|
||||
(*it)->getName(),
|
||||
Font::get(FONT_SIZE_LARGE),
|
||||
0x000000FF,
|
||||
ALIGN_CENTER);
|
||||
text->setSize(logoSize());
|
||||
e.data.logo = std::shared_ptr<GuiComponent>(text);
|
||||
|
||||
TextComponent* textSelected = new TextComponent(mWindow,
|
||||
(*it)->getName(),
|
||||
Font::get((int)(FONT_SIZE_LARGE * SELECTED_SCALE)),
|
||||
0x000000FF,
|
||||
ALIGN_CENTER);
|
||||
textSelected->setSize(logoSize());
|
||||
e.data.logoSelected = std::shared_ptr<GuiComponent>(textSelected);
|
||||
}
|
||||
|
||||
// make background extras
|
||||
e.data.backgroundExtras = std::shared_ptr<ThemeExtras>(new ThemeExtras(mWindow));
|
||||
e.data.backgroundExtras->setExtras(ThemeData::makeExtras((*it)->getTheme(), "system", mWindow));
|
||||
|
||||
this->add(e);
|
||||
}
|
||||
}
|
||||
|
||||
void SystemView::goToSystem(SystemData* system, bool animate)
|
||||
{
|
||||
setCursor(system);
|
||||
|
||||
if(!animate)
|
||||
finishAnimation(0);
|
||||
}
|
||||
|
||||
bool SystemView::input(InputConfig* config, Input input)
|
||||
{
|
||||
if(input.value != 0)
|
||||
{
|
||||
if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_r && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug"))
|
||||
{
|
||||
LOG(LogInfo) << " Reloading SystemList view";
|
||||
|
||||
// reload themes
|
||||
for(auto it = mEntries.begin(); it != mEntries.end(); it++)
|
||||
it->object->loadTheme();
|
||||
|
||||
populate();
|
||||
updateHelpPrompts();
|
||||
return true;
|
||||
}
|
||||
if(config->isMappedTo("left", input))
|
||||
{
|
||||
listInput(-1);
|
||||
return true;
|
||||
}
|
||||
if(config->isMappedTo("right", input))
|
||||
{
|
||||
listInput(1);
|
||||
return true;
|
||||
}
|
||||
if(config->isMappedTo("a", input))
|
||||
{
|
||||
stopScrolling();
|
||||
ViewController::get()->goToGameList(getSelected());
|
||||
return true;
|
||||
}
|
||||
}else{
|
||||
if(config->isMappedTo("left", input) || config->isMappedTo("right", input))
|
||||
listInput(0);
|
||||
}
|
||||
|
||||
return GuiComponent::input(config, input);
|
||||
}
|
||||
|
||||
void SystemView::update(int deltaTime)
|
||||
{
|
||||
listUpdate(deltaTime);
|
||||
GuiComponent::update(deltaTime);
|
||||
}
|
||||
|
||||
void SystemView::onCursorChanged(const CursorState& state)
|
||||
{
|
||||
// update help style
|
||||
updateHelpPrompts();
|
||||
|
||||
float startPos = mCamOffset;
|
||||
|
||||
float posMax = (float)mEntries.size();
|
||||
float target = (float)mCursor;
|
||||
|
||||
// what's the shortest way to get to our target?
|
||||
// it's one of these...
|
||||
|
||||
float endPos = target; // directly
|
||||
float dist = abs(endPos - startPos);
|
||||
|
||||
if(abs(target + posMax - startPos) < dist)
|
||||
endPos = target + posMax; // loop around the end (0 -> max)
|
||||
if(abs(target - posMax - startPos) < dist)
|
||||
endPos = target - posMax; // loop around the start (max - 1 -> -1)
|
||||
|
||||
|
||||
// animate mSystemInfo's opacity (fade out, wait, fade back in)
|
||||
|
||||
cancelAnimation(1);
|
||||
cancelAnimation(2);
|
||||
|
||||
const float infoStartOpacity = mSystemInfo.getOpacity() / 255.f;
|
||||
|
||||
Animation* infoFadeOut = new LambdaAnimation(
|
||||
[infoStartOpacity, this] (float t)
|
||||
{
|
||||
mSystemInfo.setOpacity((unsigned char)(lerp<float>(infoStartOpacity, 0.f, t) * 255));
|
||||
}, (int)(infoStartOpacity * 150));
|
||||
|
||||
unsigned int gameCount = getSelected()->getGameCount();
|
||||
|
||||
// also change the text after we've fully faded out
|
||||
setAnimation(infoFadeOut, 0, [this, gameCount] {
|
||||
std::stringstream ss;
|
||||
|
||||
// only display a game count if there are at least 2 games
|
||||
if(gameCount > 1)
|
||||
ss << gameCount << " GAMES AVAILABLE";
|
||||
|
||||
mSystemInfo.setText(ss.str());
|
||||
}, false, 1);
|
||||
|
||||
// only display a game count if there are at least 2 games
|
||||
if(gameCount > 1)
|
||||
{
|
||||
Animation* infoFadeIn = new LambdaAnimation(
|
||||
[this](float t)
|
||||
{
|
||||
mSystemInfo.setOpacity((unsigned char)(lerp<float>(0.f, 1.f, t) * 255));
|
||||
}, 300);
|
||||
|
||||
// wait 600ms to fade in
|
||||
setAnimation(infoFadeIn, 2000, nullptr, false, 2);
|
||||
}
|
||||
|
||||
// no need to animate transition, we're not going anywhere (probably mEntries.size() == 1)
|
||||
if(endPos == mCamOffset && endPos == mExtrasCamOffset)
|
||||
return;
|
||||
|
||||
Animation* anim;
|
||||
if(Settings::getInstance()->getString("TransitionStyle") == "fade")
|
||||
{
|
||||
float startExtrasFade = mExtrasFadeOpacity;
|
||||
anim = new LambdaAnimation(
|
||||
[startExtrasFade, startPos, endPos, posMax, this](float t)
|
||||
{
|
||||
t -= 1;
|
||||
float f = lerp<float>(startPos, endPos, t*t*t + 1);
|
||||
if(f < 0)
|
||||
f += posMax;
|
||||
if(f >= posMax)
|
||||
f -= posMax;
|
||||
|
||||
this->mCamOffset = f;
|
||||
|
||||
t += 1;
|
||||
if(t < 0.3f)
|
||||
this->mExtrasFadeOpacity = lerp<float>(0.0f, 1.0f, t / 0.3f + startExtrasFade);
|
||||
else if(t < 0.7f)
|
||||
this->mExtrasFadeOpacity = 1.0f;
|
||||
else
|
||||
this->mExtrasFadeOpacity = lerp<float>(1.0f, 0.0f, (t - 0.7f) / 0.3f);
|
||||
|
||||
if(t > 0.5f)
|
||||
this->mExtrasCamOffset = endPos;
|
||||
|
||||
}, 500);
|
||||
}
|
||||
else{ // slide
|
||||
anim = new LambdaAnimation(
|
||||
[startPos, endPos, posMax, this](float t)
|
||||
{
|
||||
t -= 1;
|
||||
float f = lerp<float>(startPos, endPos, t*t*t + 1);
|
||||
if(f < 0)
|
||||
f += posMax;
|
||||
if(f >= posMax)
|
||||
f -= posMax;
|
||||
|
||||
this->mCamOffset = f;
|
||||
this->mExtrasCamOffset = f;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
setAnimation(anim, 0, nullptr, false, 0);
|
||||
}
|
||||
|
||||
void SystemView::render(const Eigen::Affine3f& parentTrans)
|
||||
{
|
||||
if(size() == 0)
|
||||
return;
|
||||
|
||||
Eigen::Affine3f trans = getTransform() * parentTrans;
|
||||
|
||||
// draw the list elements (titles, backgrounds, logos)
|
||||
const float logoSizeX = logoSize().x() + LOGO_PADDING;
|
||||
|
||||
int logoCount = (int)(mSize.x() / logoSizeX) + 2; // how many logos we need to draw
|
||||
int center = (int)(mCamOffset);
|
||||
|
||||
if(mEntries.size() == 1)
|
||||
logoCount = 1;
|
||||
|
||||
// draw background extras
|
||||
Eigen::Affine3f extrasTrans = trans;
|
||||
int extrasCenter = (int)mExtrasCamOffset;
|
||||
for(int i = extrasCenter - 1; i < extrasCenter + 2; i++)
|
||||
{
|
||||
int index = i;
|
||||
while(index < 0)
|
||||
index += mEntries.size();
|
||||
while(index >= (int)mEntries.size())
|
||||
index -= mEntries.size();
|
||||
|
||||
extrasTrans.translation() = trans.translation() + Eigen::Vector3f((i - mExtrasCamOffset) * mSize.x(), 0, 0);
|
||||
|
||||
Eigen::Vector2i clipRect = Eigen::Vector2i((int)((i - mExtrasCamOffset) * mSize.x()), 0);
|
||||
Renderer::pushClipRect(clipRect, mSize.cast<int>());
|
||||
mEntries.at(index).data.backgroundExtras->render(extrasTrans);
|
||||
Renderer::popClipRect();
|
||||
}
|
||||
|
||||
// fade extras if necessary
|
||||
if(mExtrasFadeOpacity)
|
||||
{
|
||||
Renderer::setMatrix(trans);
|
||||
Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000000 | (unsigned char)(mExtrasFadeOpacity * 255));
|
||||
}
|
||||
|
||||
// draw logos
|
||||
float xOff = (mSize.x() - logoSize().x())/2 - (mCamOffset * logoSizeX);
|
||||
float yOff = (mSize.y() - logoSize().y())/2;
|
||||
|
||||
// background behind the logos
|
||||
Renderer::setMatrix(trans);
|
||||
Renderer::drawRect(0.f, (mSize.y() - BAND_HEIGHT) / 2, mSize.x(), BAND_HEIGHT, 0xFFFFFFD8);
|
||||
|
||||
Eigen::Affine3f logoTrans = trans;
|
||||
for(int i = center - logoCount/2; i < center + logoCount/2 + 1; i++)
|
||||
{
|
||||
int index = i;
|
||||
while(index < 0)
|
||||
index += mEntries.size();
|
||||
while(index >= (int)mEntries.size())
|
||||
index -= mEntries.size();
|
||||
|
||||
logoTrans.translation() = trans.translation() + Eigen::Vector3f(i * logoSizeX + xOff, yOff, 0);
|
||||
|
||||
if(index == mCursor) //scale our selection up
|
||||
{
|
||||
// selected
|
||||
const std::shared_ptr<GuiComponent>& comp = mEntries.at(index).data.logoSelected;
|
||||
comp->setOpacity(0xFF);
|
||||
comp->render(logoTrans);
|
||||
}else{
|
||||
// not selected
|
||||
const std::shared_ptr<GuiComponent>& comp = mEntries.at(index).data.logo;
|
||||
comp->setOpacity(0x80);
|
||||
comp->render(logoTrans);
|
||||
}
|
||||
}
|
||||
|
||||
Renderer::setMatrix(trans);
|
||||
Renderer::drawRect(mSystemInfo.getPosition().x(), mSystemInfo.getPosition().y() - 1, mSize.x(), mSystemInfo.getSize().y(), 0xDDDDDD00 | (unsigned char)(mSystemInfo.getOpacity() / 255.f * 0xD8));
|
||||
mSystemInfo.render(trans);
|
||||
}
|
||||
|
||||
std::vector<HelpPrompt> SystemView::getHelpPrompts()
|
||||
{
|
||||
std::vector<HelpPrompt> prompts;
|
||||
prompts.push_back(HelpPrompt("left/right", "choose"));
|
||||
prompts.push_back(HelpPrompt("a", "select"));
|
||||
return prompts;
|
||||
}
|
||||
|
||||
HelpStyle SystemView::getHelpStyle()
|
||||
{
|
||||
HelpStyle style;
|
||||
style.applyTheme(mEntries.at(mCursor).object->getTheme(), "system");
|
||||
return style;
|
||||
}
|
48
es-app/src/views/SystemView.h
Normal file
48
es-app/src/views/SystemView.h
Normal file
|
@ -0,0 +1,48 @@
|
|||
#pragma once
|
||||
|
||||
#include "GuiComponent.h"
|
||||
#include "components/ImageComponent.h"
|
||||
#include "components/TextComponent.h"
|
||||
#include "components/ScrollableContainer.h"
|
||||
#include "components/IList.h"
|
||||
#include "resources/TextureResource.h"
|
||||
|
||||
class SystemData;
|
||||
class AnimatedImageComponent;
|
||||
|
||||
struct SystemViewData
|
||||
{
|
||||
std::shared_ptr<GuiComponent> logo;
|
||||
std::shared_ptr<GuiComponent> logoSelected;
|
||||
std::shared_ptr<ThemeExtras> backgroundExtras;
|
||||
};
|
||||
|
||||
class SystemView : public IList<SystemViewData, SystemData*>
|
||||
{
|
||||
public:
|
||||
SystemView(Window* window);
|
||||
|
||||
void goToSystem(SystemData* system, bool animate);
|
||||
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
void update(int deltaTime) override;
|
||||
void render(const Eigen::Affine3f& parentTrans) override;
|
||||
|
||||
std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
virtual HelpStyle getHelpStyle() override;
|
||||
|
||||
protected:
|
||||
void onCursorChanged(const CursorState& state) override;
|
||||
|
||||
private:
|
||||
inline Eigen::Vector2f logoSize() const { return Eigen::Vector2f(mSize.x() * 0.25f, mSize.y() * 0.155f); }
|
||||
|
||||
void populate();
|
||||
|
||||
TextComponent mSystemInfo;
|
||||
|
||||
// unit is list index
|
||||
float mCamOffset;
|
||||
float mExtrasCamOffset;
|
||||
float mExtrasFadeOpacity;
|
||||
};
|
403
es-app/src/views/ViewController.cpp
Normal file
403
es-app/src/views/ViewController.cpp
Normal file
|
@ -0,0 +1,403 @@
|
|||
#include "views/ViewController.h"
|
||||
#include "Log.h"
|
||||
#include "SystemData.h"
|
||||
#include "Settings.h"
|
||||
|
||||
#include "views/gamelist/BasicGameListView.h"
|
||||
#include "views/gamelist/DetailedGameListView.h"
|
||||
#include "views/gamelist/GridGameListView.h"
|
||||
#include "guis/GuiMenu.h"
|
||||
#include "guis/GuiMsgBox.h"
|
||||
#include "animations/LaunchAnimation.h"
|
||||
#include "animations/MoveCameraAnimation.h"
|
||||
#include "animations/LambdaAnimation.h"
|
||||
|
||||
ViewController* ViewController::sInstance = NULL;
|
||||
|
||||
ViewController* ViewController::get()
|
||||
{
|
||||
assert(sInstance);
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
void ViewController::init(Window* window)
|
||||
{
|
||||
assert(!sInstance);
|
||||
sInstance = new ViewController(window);
|
||||
}
|
||||
|
||||
ViewController::ViewController(Window* window)
|
||||
: GuiComponent(window), mCurrentView(nullptr), mCamera(Eigen::Affine3f::Identity()), mFadeOpacity(0), mLockInput(false)
|
||||
{
|
||||
mState.viewing = NOTHING;
|
||||
}
|
||||
|
||||
ViewController::~ViewController()
|
||||
{
|
||||
assert(sInstance == this);
|
||||
sInstance = NULL;
|
||||
}
|
||||
|
||||
void ViewController::goToStart()
|
||||
{
|
||||
// TODO
|
||||
/* mState.viewing = START_SCREEN;
|
||||
mCurrentView.reset();
|
||||
playViewTransition(); */
|
||||
goToSystemView(SystemData::sSystemVector.at(0));
|
||||
}
|
||||
|
||||
int ViewController::getSystemId(SystemData* system)
|
||||
{
|
||||
std::vector<SystemData*>& sysVec = SystemData::sSystemVector;
|
||||
return std::find(sysVec.begin(), sysVec.end(), system) - sysVec.begin();
|
||||
}
|
||||
|
||||
void ViewController::goToSystemView(SystemData* system)
|
||||
{
|
||||
mState.viewing = SYSTEM_SELECT;
|
||||
mState.system = system;
|
||||
|
||||
auto systemList = getSystemListView();
|
||||
systemList->setPosition(getSystemId(system) * (float)Renderer::getScreenWidth(), systemList->getPosition().y());
|
||||
|
||||
systemList->goToSystem(system, false);
|
||||
mCurrentView = systemList;
|
||||
|
||||
playViewTransition();
|
||||
}
|
||||
|
||||
void ViewController::goToNextGameList()
|
||||
{
|
||||
assert(mState.viewing == GAME_LIST);
|
||||
SystemData* system = getState().getSystem();
|
||||
assert(system);
|
||||
goToGameList(system->getNext());
|
||||
}
|
||||
|
||||
void ViewController::goToPrevGameList()
|
||||
{
|
||||
assert(mState.viewing == GAME_LIST);
|
||||
SystemData* system = getState().getSystem();
|
||||
assert(system);
|
||||
goToGameList(system->getPrev());
|
||||
}
|
||||
|
||||
void ViewController::goToGameList(SystemData* system)
|
||||
{
|
||||
if(mState.viewing == SYSTEM_SELECT)
|
||||
{
|
||||
// move system list
|
||||
auto sysList = getSystemListView();
|
||||
float offX = sysList->getPosition().x();
|
||||
int sysId = getSystemId(system);
|
||||
sysList->setPosition(sysId * (float)Renderer::getScreenWidth(), sysList->getPosition().y());
|
||||
offX = sysList->getPosition().x() - offX;
|
||||
mCamera.translation().x() -= offX;
|
||||
}
|
||||
|
||||
mState.viewing = GAME_LIST;
|
||||
mState.system = system;
|
||||
|
||||
mCurrentView = getGameListView(system);
|
||||
playViewTransition();
|
||||
}
|
||||
|
||||
void ViewController::playViewTransition()
|
||||
{
|
||||
Eigen::Vector3f target(Eigen::Vector3f::Identity());
|
||||
if(mCurrentView)
|
||||
target = mCurrentView->getPosition();
|
||||
|
||||
// no need to animate, we're not going anywhere (probably goToNextGamelist() or goToPrevGamelist() when there's only 1 system)
|
||||
if(target == -mCamera.translation() && !isAnimationPlaying(0))
|
||||
return;
|
||||
|
||||
if(Settings::getInstance()->getString("TransitionStyle") == "fade")
|
||||
{
|
||||
// fade
|
||||
// stop whatever's currently playing, leaving mFadeOpacity wherever it is
|
||||
cancelAnimation(0);
|
||||
|
||||
auto fadeFunc = [this](float t) {
|
||||
mFadeOpacity = lerp<float>(0, 1, t);
|
||||
};
|
||||
|
||||
const static int FADE_DURATION = 240; // fade in/out time
|
||||
const static int FADE_WAIT = 320; // time to wait between in/out
|
||||
setAnimation(new LambdaAnimation(fadeFunc, FADE_DURATION), 0, [this, fadeFunc, target] {
|
||||
this->mCamera.translation() = -target;
|
||||
updateHelpPrompts();
|
||||
setAnimation(new LambdaAnimation(fadeFunc, FADE_DURATION), FADE_WAIT, nullptr, true);
|
||||
});
|
||||
|
||||
// fast-forward animation if we're partway faded
|
||||
if(target == -mCamera.translation())
|
||||
{
|
||||
// not changing screens, so cancel the first half entirely
|
||||
advanceAnimation(0, FADE_DURATION);
|
||||
advanceAnimation(0, FADE_WAIT);
|
||||
advanceAnimation(0, FADE_DURATION - (int)(mFadeOpacity * FADE_DURATION));
|
||||
}else{
|
||||
advanceAnimation(0, (int)(mFadeOpacity * FADE_DURATION));
|
||||
}
|
||||
}else{
|
||||
// slide
|
||||
setAnimation(new MoveCameraAnimation(mCamera, target));
|
||||
updateHelpPrompts(); // update help prompts immediately
|
||||
}
|
||||
}
|
||||
|
||||
void ViewController::onFileChanged(FileData* file, FileChangeType change)
|
||||
{
|
||||
auto it = mGameListViews.find(file->getSystem());
|
||||
if(it != mGameListViews.end())
|
||||
it->second->onFileChanged(file, change);
|
||||
}
|
||||
|
||||
void ViewController::launch(FileData* game, Eigen::Vector3f center)
|
||||
{
|
||||
if(game->getType() != GAME)
|
||||
{
|
||||
LOG(LogError) << "tried to launch something that isn't a game";
|
||||
return;
|
||||
}
|
||||
|
||||
Eigen::Affine3f origCamera = mCamera;
|
||||
origCamera.translation() = -mCurrentView->getPosition();
|
||||
|
||||
center += mCurrentView->getPosition();
|
||||
stopAnimation(1); // make sure the fade in isn't still playing
|
||||
mLockInput = true;
|
||||
|
||||
if(Settings::getInstance()->getString("TransitionStyle") == "fade")
|
||||
{
|
||||
// fade out, launch game, fade back in
|
||||
auto fadeFunc = [this](float t) {
|
||||
//t -= 1;
|
||||
//mFadeOpacity = lerp<float>(0.0f, 1.0f, t*t*t + 1);
|
||||
mFadeOpacity = lerp<float>(0.0f, 1.0f, t);
|
||||
};
|
||||
setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this, game, fadeFunc]
|
||||
{
|
||||
game->getSystem()->launchGame(mWindow, game);
|
||||
mLockInput = false;
|
||||
setAnimation(new LambdaAnimation(fadeFunc, 800), 0, nullptr, true);
|
||||
this->onFileChanged(game, FILE_METADATA_CHANGED);
|
||||
});
|
||||
}else{
|
||||
// move camera to zoom in on center + fade out, launch game, come back in
|
||||
setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 1500), 0, [this, origCamera, center, game]
|
||||
{
|
||||
game->getSystem()->launchGame(mWindow, game);
|
||||
mCamera = origCamera;
|
||||
mLockInput = false;
|
||||
setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 600), 0, nullptr, true);
|
||||
this->onFileChanged(game, FILE_METADATA_CHANGED);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<IGameListView> ViewController::getGameListView(SystemData* system)
|
||||
{
|
||||
//if we already made one, return that one
|
||||
auto exists = mGameListViews.find(system);
|
||||
if(exists != mGameListViews.end())
|
||||
return exists->second;
|
||||
|
||||
//if we didn't, make it, remember it, and return it
|
||||
std::shared_ptr<IGameListView> view;
|
||||
|
||||
//decide type
|
||||
bool detailed = false;
|
||||
std::vector<FileData*> files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER);
|
||||
for(auto it = files.begin(); it != files.end(); it++)
|
||||
{
|
||||
if(!(*it)->getThumbnailPath().empty())
|
||||
{
|
||||
detailed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(detailed)
|
||||
view = std::shared_ptr<IGameListView>(new DetailedGameListView(mWindow, system->getRootFolder()));
|
||||
else
|
||||
view = std::shared_ptr<IGameListView>(new BasicGameListView(mWindow, system->getRootFolder()));
|
||||
|
||||
// uncomment for experimental "image grid" view
|
||||
//view = std::shared_ptr<IGameListView>(new GridGameListView(mWindow, system->getRootFolder()));
|
||||
|
||||
view->setTheme(system->getTheme());
|
||||
|
||||
std::vector<SystemData*>& sysVec = SystemData::sSystemVector;
|
||||
int id = std::find(sysVec.begin(), sysVec.end(), system) - sysVec.begin();
|
||||
view->setPosition(id * (float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight() * 2);
|
||||
|
||||
addChild(view.get());
|
||||
|
||||
mGameListViews[system] = view;
|
||||
return view;
|
||||
}
|
||||
|
||||
std::shared_ptr<SystemView> ViewController::getSystemListView()
|
||||
{
|
||||
//if we already made one, return that one
|
||||
if(mSystemListView)
|
||||
return mSystemListView;
|
||||
|
||||
mSystemListView = std::shared_ptr<SystemView>(new SystemView(mWindow));
|
||||
addChild(mSystemListView.get());
|
||||
mSystemListView->setPosition(0, (float)Renderer::getScreenHeight());
|
||||
return mSystemListView;
|
||||
}
|
||||
|
||||
|
||||
bool ViewController::input(InputConfig* config, Input input)
|
||||
{
|
||||
if(mLockInput)
|
||||
return true;
|
||||
|
||||
// open menu
|
||||
if(config->isMappedTo("start", input) && input.value != 0)
|
||||
{
|
||||
// open menu
|
||||
mWindow->pushGui(new GuiMenu(mWindow));
|
||||
return true;
|
||||
}
|
||||
|
||||
if(mCurrentView)
|
||||
return mCurrentView->input(config, input);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ViewController::update(int deltaTime)
|
||||
{
|
||||
if(mCurrentView)
|
||||
{
|
||||
mCurrentView->update(deltaTime);
|
||||
}
|
||||
|
||||
updateSelf(deltaTime);
|
||||
}
|
||||
|
||||
void ViewController::render(const Eigen::Affine3f& parentTrans)
|
||||
{
|
||||
Eigen::Affine3f trans = mCamera * parentTrans;
|
||||
|
||||
// camera position, position + size
|
||||
Eigen::Vector3f viewStart = trans.inverse().translation();
|
||||
Eigen::Vector3f viewEnd = trans.inverse() * Eigen::Vector3f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight(), 0);
|
||||
|
||||
// draw systemview
|
||||
getSystemListView()->render(trans);
|
||||
|
||||
// draw gamelists
|
||||
for(auto it = mGameListViews.begin(); it != mGameListViews.end(); it++)
|
||||
{
|
||||
// clipping
|
||||
Eigen::Vector3f guiStart = it->second->getPosition();
|
||||
Eigen::Vector3f guiEnd = it->second->getPosition() + Eigen::Vector3f(it->second->getSize().x(), it->second->getSize().y(), 0);
|
||||
|
||||
if(guiEnd.x() >= viewStart.x() && guiEnd.y() >= viewStart.y() &&
|
||||
guiStart.x() <= viewEnd.x() && guiStart.y() <= viewEnd.y())
|
||||
it->second->render(trans);
|
||||
}
|
||||
|
||||
if(mWindow->peekGui() == this)
|
||||
mWindow->renderHelpPromptsEarly();
|
||||
|
||||
// fade out
|
||||
if(mFadeOpacity)
|
||||
{
|
||||
Renderer::setMatrix(parentTrans);
|
||||
Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(mFadeOpacity * 255));
|
||||
}
|
||||
}
|
||||
|
||||
void ViewController::preload()
|
||||
{
|
||||
for(auto it = SystemData::sSystemVector.begin(); it != SystemData::sSystemVector.end(); it++)
|
||||
{
|
||||
getGameListView(*it);
|
||||
}
|
||||
}
|
||||
|
||||
void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme)
|
||||
{
|
||||
for(auto it = mGameListViews.begin(); it != mGameListViews.end(); it++)
|
||||
{
|
||||
if(it->second.get() == view)
|
||||
{
|
||||
bool isCurrent = (mCurrentView == it->second);
|
||||
SystemData* system = it->first;
|
||||
FileData* cursor = view->getCursor();
|
||||
mGameListViews.erase(it);
|
||||
|
||||
if(reloadTheme)
|
||||
system->loadTheme();
|
||||
|
||||
std::shared_ptr<IGameListView> newView = getGameListView(system);
|
||||
newView->setCursor(cursor);
|
||||
|
||||
if(isCurrent)
|
||||
mCurrentView = newView;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ViewController::reloadAll()
|
||||
{
|
||||
std::map<SystemData*, FileData*> cursorMap;
|
||||
for(auto it = mGameListViews.begin(); it != mGameListViews.end(); it++)
|
||||
{
|
||||
cursorMap[it->first] = it->second->getCursor();
|
||||
}
|
||||
mGameListViews.clear();
|
||||
|
||||
for(auto it = cursorMap.begin(); it != cursorMap.end(); it++)
|
||||
{
|
||||
it->first->loadTheme();
|
||||
getGameListView(it->first)->setCursor(it->second);
|
||||
}
|
||||
|
||||
mSystemListView.reset();
|
||||
getSystemListView();
|
||||
|
||||
// update mCurrentView since the pointers changed
|
||||
if(mState.viewing == GAME_LIST)
|
||||
{
|
||||
mCurrentView = getGameListView(mState.getSystem());
|
||||
}else if(mState.viewing == SYSTEM_SELECT)
|
||||
{
|
||||
mSystemListView->goToSystem(mState.getSystem(), false);
|
||||
mCurrentView = mSystemListView;
|
||||
}else{
|
||||
goToSystemView(SystemData::sSystemVector.front());
|
||||
}
|
||||
|
||||
updateHelpPrompts();
|
||||
}
|
||||
|
||||
std::vector<HelpPrompt> ViewController::getHelpPrompts()
|
||||
{
|
||||
std::vector<HelpPrompt> prompts;
|
||||
if(!mCurrentView)
|
||||
return prompts;
|
||||
|
||||
prompts = mCurrentView->getHelpPrompts();
|
||||
prompts.push_back(HelpPrompt("start", "menu"));
|
||||
|
||||
return prompts;
|
||||
}
|
||||
|
||||
HelpStyle ViewController::getHelpStyle()
|
||||
{
|
||||
if(!mCurrentView)
|
||||
return GuiComponent::getHelpStyle();
|
||||
|
||||
return mCurrentView->getHelpStyle();
|
||||
}
|
87
es-app/src/views/ViewController.h
Normal file
87
es-app/src/views/ViewController.h
Normal file
|
@ -0,0 +1,87 @@
|
|||
#pragma once
|
||||
|
||||
#include "views/gamelist/IGameListView.h"
|
||||
#include "views/SystemView.h"
|
||||
|
||||
class SystemData;
|
||||
|
||||
// Used to smoothly transition the camera between multiple views (e.g. from system to system, from gamelist to gamelist).
|
||||
class ViewController : public GuiComponent
|
||||
{
|
||||
public:
|
||||
static void init(Window* window);
|
||||
static ViewController* get();
|
||||
|
||||
virtual ~ViewController();
|
||||
|
||||
// Try to completely populate the GameListView map.
|
||||
// Caches things so there's no pauses during transitions.
|
||||
void preload();
|
||||
|
||||
// If a basic view detected a metadata change, it can request to recreate
|
||||
// the current gamelist view (as it may change to be detailed).
|
||||
void reloadGameListView(IGameListView* gamelist, bool reloadTheme = false);
|
||||
inline void reloadGameListView(SystemData* system, bool reloadTheme = false) { reloadGameListView(getGameListView(system).get(), reloadTheme); }
|
||||
void reloadAll(); // Reload everything with a theme. Used when the "ThemeSet" setting changes.
|
||||
|
||||
// Navigation.
|
||||
void goToNextGameList();
|
||||
void goToPrevGameList();
|
||||
void goToGameList(SystemData* system);
|
||||
void goToSystemView(SystemData* system);
|
||||
void goToStart();
|
||||
|
||||
void onFileChanged(FileData* file, FileChangeType change);
|
||||
|
||||
// Plays a nice launch effect and launches the game at the end of it.
|
||||
// Once the game terminates, plays a return effect.
|
||||
void launch(FileData* game, Eigen::Vector3f centerCameraOn = Eigen::Vector3f(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0));
|
||||
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
void update(int deltaTime) override;
|
||||
void render(const Eigen::Affine3f& parentTrans) override;
|
||||
|
||||
enum ViewMode
|
||||
{
|
||||
NOTHING,
|
||||
START_SCREEN,
|
||||
SYSTEM_SELECT,
|
||||
GAME_LIST
|
||||
};
|
||||
|
||||
struct State
|
||||
{
|
||||
ViewMode viewing;
|
||||
|
||||
inline SystemData* getSystem() const { assert(viewing == GAME_LIST || viewing == SYSTEM_SELECT); return system; }
|
||||
|
||||
private:
|
||||
friend ViewController;
|
||||
SystemData* system;
|
||||
};
|
||||
|
||||
inline const State& getState() const { return mState; }
|
||||
|
||||
virtual std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
virtual HelpStyle getHelpStyle() override;
|
||||
|
||||
std::shared_ptr<IGameListView> getGameListView(SystemData* system);
|
||||
std::shared_ptr<SystemView> getSystemListView();
|
||||
|
||||
private:
|
||||
ViewController(Window* window);
|
||||
static ViewController* sInstance;
|
||||
|
||||
void playViewTransition();
|
||||
int getSystemId(SystemData* system);
|
||||
|
||||
std::shared_ptr<GuiComponent> mCurrentView;
|
||||
std::map< SystemData*, std::shared_ptr<IGameListView> > mGameListViews;
|
||||
std::shared_ptr<SystemView> mSystemListView;
|
||||
|
||||
Eigen::Affine3f mCamera;
|
||||
float mFadeOpacity;
|
||||
bool mLockInput;
|
||||
|
||||
State mState;
|
||||
};
|
100
es-app/src/views/gamelist/BasicGameListView.cpp
Normal file
100
es-app/src/views/gamelist/BasicGameListView.cpp
Normal file
|
@ -0,0 +1,100 @@
|
|||
#include "views/gamelist/BasicGameListView.h"
|
||||
#include "views/ViewController.h"
|
||||
#include "Renderer.h"
|
||||
#include "Window.h"
|
||||
#include "ThemeData.h"
|
||||
#include "SystemData.h"
|
||||
#include "Settings.h"
|
||||
|
||||
BasicGameListView::BasicGameListView(Window* window, FileData* root)
|
||||
: ISimpleGameListView(window, root), mList(window)
|
||||
{
|
||||
mList.setSize(mSize.x(), mSize.y() * 0.8f);
|
||||
mList.setPosition(0, mSize.y() * 0.2f);
|
||||
addChild(&mList);
|
||||
|
||||
populateList(root->getChildren());
|
||||
}
|
||||
|
||||
void BasicGameListView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
|
||||
{
|
||||
ISimpleGameListView::onThemeChanged(theme);
|
||||
using namespace ThemeFlags;
|
||||
mList.applyTheme(theme, getName(), "gamelist", ALL);
|
||||
}
|
||||
|
||||
void BasicGameListView::onFileChanged(FileData* file, FileChangeType change)
|
||||
{
|
||||
if(change == FILE_METADATA_CHANGED)
|
||||
{
|
||||
// might switch to a detailed view
|
||||
ViewController::get()->reloadGameListView(this);
|
||||
return;
|
||||
}
|
||||
|
||||
ISimpleGameListView::onFileChanged(file, change);
|
||||
}
|
||||
|
||||
void BasicGameListView::populateList(const std::vector<FileData*>& files)
|
||||
{
|
||||
mList.clear();
|
||||
|
||||
mHeaderText.setText(files.at(0)->getSystem()->getFullName());
|
||||
|
||||
for(auto it = files.begin(); it != files.end(); it++)
|
||||
{
|
||||
mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER));
|
||||
}
|
||||
}
|
||||
|
||||
FileData* BasicGameListView::getCursor()
|
||||
{
|
||||
return mList.getSelected();
|
||||
}
|
||||
|
||||
void BasicGameListView::setCursor(FileData* cursor)
|
||||
{
|
||||
if(!mList.setCursor(cursor))
|
||||
{
|
||||
populateList(cursor->getParent()->getChildren());
|
||||
mList.setCursor(cursor);
|
||||
|
||||
// update our cursor stack in case our cursor just got set to some folder we weren't in before
|
||||
if(mCursorStack.empty() || mCursorStack.top() != cursor->getParent())
|
||||
{
|
||||
std::stack<FileData*> tmp;
|
||||
FileData* ptr = cursor->getParent();
|
||||
while(ptr && ptr != mRoot)
|
||||
{
|
||||
tmp.push(ptr);
|
||||
ptr = ptr->getParent();
|
||||
}
|
||||
|
||||
// flip the stack and put it in mCursorStack
|
||||
mCursorStack = std::stack<FileData*>();
|
||||
while(!tmp.empty())
|
||||
{
|
||||
mCursorStack.push(tmp.top());
|
||||
tmp.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BasicGameListView::launch(FileData* game)
|
||||
{
|
||||
ViewController::get()->launch(game);
|
||||
}
|
||||
|
||||
std::vector<HelpPrompt> BasicGameListView::getHelpPrompts()
|
||||
{
|
||||
std::vector<HelpPrompt> prompts;
|
||||
|
||||
if(Settings::getInstance()->getBool("QuickSystemSelect"))
|
||||
prompts.push_back(HelpPrompt("left/right", "system"));
|
||||
prompts.push_back(HelpPrompt("up/down", "choose"));
|
||||
prompts.push_back(HelpPrompt("a", "launch"));
|
||||
prompts.push_back(HelpPrompt("b", "back"));
|
||||
prompts.push_back(HelpPrompt("select", "options"));
|
||||
return prompts;
|
||||
}
|
28
es-app/src/views/gamelist/BasicGameListView.h
Normal file
28
es-app/src/views/gamelist/BasicGameListView.h
Normal file
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include "views/gamelist/ISimpleGameListView.h"
|
||||
#include "components/TextListComponent.h"
|
||||
|
||||
class BasicGameListView : public ISimpleGameListView
|
||||
{
|
||||
public:
|
||||
BasicGameListView(Window* window, FileData* root);
|
||||
|
||||
// Called when a FileData* is added, has its metadata changed, or is removed
|
||||
virtual void onFileChanged(FileData* file, FileChangeType change);
|
||||
|
||||
virtual void onThemeChanged(const std::shared_ptr<ThemeData>& theme);
|
||||
|
||||
virtual FileData* getCursor() override;
|
||||
virtual void setCursor(FileData* file) override;
|
||||
|
||||
virtual const char* getName() const override { return "basic"; }
|
||||
|
||||
virtual std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
|
||||
protected:
|
||||
virtual void populateList(const std::vector<FileData*>& files) override;
|
||||
virtual void launch(FileData* game) override;
|
||||
|
||||
TextListComponent<FileData*> mList;
|
||||
};
|
270
es-app/src/views/gamelist/DetailedGameListView.cpp
Normal file
270
es-app/src/views/gamelist/DetailedGameListView.cpp
Normal file
|
@ -0,0 +1,270 @@
|
|||
#include "views/gamelist/DetailedGameListView.h"
|
||||
#include "views/ViewController.h"
|
||||
#include "Window.h"
|
||||
#include "animations/LambdaAnimation.h"
|
||||
|
||||
DetailedGameListView::DetailedGameListView(Window* window, FileData* root) :
|
||||
BasicGameListView(window, root),
|
||||
mDescContainer(window), mDescription(window),
|
||||
mImage(window),
|
||||
|
||||
mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window),
|
||||
mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window),
|
||||
|
||||
mRating(window), mReleaseDate(window), mDeveloper(window), mPublisher(window),
|
||||
mGenre(window), mPlayers(window), mLastPlayed(window), mPlayCount(window)
|
||||
{
|
||||
//mHeaderImage.setPosition(mSize.x() * 0.25f, 0);
|
||||
|
||||
const float padding = 0.01f;
|
||||
|
||||
mList.setPosition(mSize.x() * (0.50f + padding), mList.getPosition().y());
|
||||
mList.setSize(mSize.x() * (0.50f - padding), mList.getSize().y());
|
||||
mList.setAlignment(TextListComponent<FileData*>::ALIGN_LEFT);
|
||||
mList.setCursorChangedCallback([&](const CursorState& state) { updateInfoPanel(); });
|
||||
|
||||
// image
|
||||
mImage.setOrigin(0.5f, 0.5f);
|
||||
mImage.setPosition(mSize.x() * 0.25f, mList.getPosition().y() + mSize.y() * 0.2125f);
|
||||
mImage.setMaxSize(mSize.x() * (0.50f - 2*padding), mSize.y() * 0.4f);
|
||||
addChild(&mImage);
|
||||
|
||||
// metadata labels + values
|
||||
mLblRating.setText("Rating: ");
|
||||
addChild(&mLblRating);
|
||||
addChild(&mRating);
|
||||
mLblReleaseDate.setText("Released: ");
|
||||
addChild(&mLblReleaseDate);
|
||||
addChild(&mReleaseDate);
|
||||
mLblDeveloper.setText("Developer: ");
|
||||
addChild(&mLblDeveloper);
|
||||
addChild(&mDeveloper);
|
||||
mLblPublisher.setText("Publisher: ");
|
||||
addChild(&mLblPublisher);
|
||||
addChild(&mPublisher);
|
||||
mLblGenre.setText("Genre: ");
|
||||
addChild(&mLblGenre);
|
||||
addChild(&mGenre);
|
||||
mLblPlayers.setText("Players: ");
|
||||
addChild(&mLblPlayers);
|
||||
addChild(&mPlayers);
|
||||
mLblLastPlayed.setText("Last played: ");
|
||||
addChild(&mLblLastPlayed);
|
||||
mLastPlayed.setDisplayMode(DateTimeComponent::DISP_RELATIVE_TO_NOW);
|
||||
addChild(&mLastPlayed);
|
||||
mLblPlayCount.setText("Times played: ");
|
||||
addChild(&mLblPlayCount);
|
||||
addChild(&mPlayCount);
|
||||
|
||||
mDescContainer.setPosition(mSize.x() * padding, mSize.y() * 0.65f);
|
||||
mDescContainer.setSize(mSize.x() * (0.50f - 2*padding), mSize.y() - mDescContainer.getPosition().y());
|
||||
mDescContainer.setAutoScroll(true);
|
||||
addChild(&mDescContainer);
|
||||
|
||||
mDescription.setFont(Font::get(FONT_SIZE_SMALL));
|
||||
mDescription.setSize(mDescContainer.getSize().x(), 0);
|
||||
mDescContainer.addChild(&mDescription);
|
||||
|
||||
|
||||
initMDLabels();
|
||||
initMDValues();
|
||||
updateInfoPanel();
|
||||
}
|
||||
|
||||
void DetailedGameListView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
|
||||
{
|
||||
BasicGameListView::onThemeChanged(theme);
|
||||
|
||||
using namespace ThemeFlags;
|
||||
mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE);
|
||||
|
||||
initMDLabels();
|
||||
std::vector<TextComponent*> labels = getMDLabels();
|
||||
assert(labels.size() == 8);
|
||||
const char* lblElements[8] = {
|
||||
"md_lbl_rating", "md_lbl_releasedate", "md_lbl_developer", "md_lbl_publisher",
|
||||
"md_lbl_genre", "md_lbl_players", "md_lbl_lastplayed", "md_lbl_playcount"
|
||||
};
|
||||
|
||||
for(unsigned int i = 0; i < labels.size(); i++)
|
||||
{
|
||||
labels[i]->applyTheme(theme, getName(), lblElements[i], ALL);
|
||||
}
|
||||
|
||||
|
||||
initMDValues();
|
||||
std::vector<GuiComponent*> values = getMDValues();
|
||||
assert(values.size() == 8);
|
||||
const char* valElements[8] = {
|
||||
"md_rating", "md_releasedate", "md_developer", "md_publisher",
|
||||
"md_genre", "md_players", "md_lastplayed", "md_playcount"
|
||||
};
|
||||
|
||||
for(unsigned int i = 0; i < values.size(); i++)
|
||||
{
|
||||
values[i]->applyTheme(theme, getName(), valElements[i], ALL ^ ThemeFlags::TEXT);
|
||||
}
|
||||
|
||||
mDescContainer.applyTheme(theme, getName(), "md_description", POSITION | ThemeFlags::SIZE);
|
||||
mDescription.setSize(mDescContainer.getSize().x(), 0);
|
||||
mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | TEXT));
|
||||
}
|
||||
|
||||
void DetailedGameListView::initMDLabels()
|
||||
{
|
||||
using namespace Eigen;
|
||||
|
||||
std::vector<TextComponent*> components = getMDLabels();
|
||||
|
||||
const unsigned int colCount = 2;
|
||||
const unsigned int rowCount = components.size() / 2;
|
||||
|
||||
Vector3f start(mSize.x() * 0.01f, mSize.y() * 0.625f, 0.0f);
|
||||
|
||||
const float colSize = (mSize.x() * 0.48f) / colCount;
|
||||
const float rowPadding = 0.01f * mSize.y();
|
||||
|
||||
for(unsigned int i = 0; i < components.size(); i++)
|
||||
{
|
||||
const unsigned int row = i % rowCount;
|
||||
Vector3f pos(0.0f, 0.0f, 0.0f);
|
||||
if(row == 0)
|
||||
{
|
||||
pos = start + Vector3f(colSize * (i / rowCount), 0, 0);
|
||||
}else{
|
||||
// work from the last component
|
||||
GuiComponent* lc = components[i-1];
|
||||
pos = lc->getPosition() + Vector3f(0, lc->getSize().y() + rowPadding, 0);
|
||||
}
|
||||
|
||||
components[i]->setFont(Font::get(FONT_SIZE_SMALL));
|
||||
components[i]->setPosition(pos);
|
||||
}
|
||||
}
|
||||
|
||||
void DetailedGameListView::initMDValues()
|
||||
{
|
||||
using namespace Eigen;
|
||||
|
||||
std::vector<TextComponent*> labels = getMDLabels();
|
||||
std::vector<GuiComponent*> values = getMDValues();
|
||||
|
||||
std::shared_ptr<Font> defaultFont = Font::get(FONT_SIZE_SMALL);
|
||||
mRating.setSize(defaultFont->getHeight() * 5.0f, (float)defaultFont->getHeight());
|
||||
mReleaseDate.setFont(defaultFont);
|
||||
mDeveloper.setFont(defaultFont);
|
||||
mPublisher.setFont(defaultFont);
|
||||
mGenre.setFont(defaultFont);
|
||||
mPlayers.setFont(defaultFont);
|
||||
mLastPlayed.setFont(defaultFont);
|
||||
mPlayCount.setFont(defaultFont);
|
||||
|
||||
float bottom = 0.0f;
|
||||
|
||||
const float colSize = (mSize.x() * 0.48f) / 2;
|
||||
for(unsigned int i = 0; i < labels.size(); i++)
|
||||
{
|
||||
const float heightDiff = (labels[i]->getSize().y() - values[i]->getSize().y()) / 2;
|
||||
values[i]->setPosition(labels[i]->getPosition() + Vector3f(labels[i]->getSize().x(), heightDiff, 0));
|
||||
values[i]->setSize(colSize - labels[i]->getSize().x(), values[i]->getSize().y());
|
||||
|
||||
float testBot = values[i]->getPosition().y() + values[i]->getSize().y();
|
||||
if(testBot > bottom)
|
||||
bottom = testBot;
|
||||
}
|
||||
|
||||
mDescContainer.setPosition(mDescContainer.getPosition().x(), bottom + mSize.y() * 0.01f);
|
||||
mDescContainer.setSize(mDescContainer.getSize().x(), mSize.y() - mDescContainer.getPosition().y());
|
||||
}
|
||||
|
||||
void DetailedGameListView::updateInfoPanel()
|
||||
{
|
||||
FileData* file = (mList.size() == 0 || mList.isScrolling()) ? NULL : mList.getSelected();
|
||||
|
||||
bool fadingOut;
|
||||
if(file == NULL)
|
||||
{
|
||||
//mImage.setImage("");
|
||||
//mDescription.setText("");
|
||||
fadingOut = true;
|
||||
}else{
|
||||
mImage.setImage(file->metadata.get("image"));
|
||||
mDescription.setText(file->metadata.get("desc"));
|
||||
mDescContainer.reset();
|
||||
|
||||
if(file->getType() == GAME)
|
||||
{
|
||||
mRating.setValue(file->metadata.get("rating"));
|
||||
mReleaseDate.setValue(file->metadata.get("releasedate"));
|
||||
mDeveloper.setValue(file->metadata.get("developer"));
|
||||
mPublisher.setValue(file->metadata.get("publisher"));
|
||||
mGenre.setValue(file->metadata.get("genre"));
|
||||
mPlayers.setValue(file->metadata.get("players"));
|
||||
mLastPlayed.setValue(file->metadata.get("lastplayed"));
|
||||
mPlayCount.setValue(file->metadata.get("playcount"));
|
||||
}
|
||||
|
||||
fadingOut = false;
|
||||
}
|
||||
|
||||
std::vector<GuiComponent*> comps = getMDValues();
|
||||
comps.push_back(&mImage);
|
||||
comps.push_back(&mDescription);
|
||||
std::vector<TextComponent*> labels = getMDLabels();
|
||||
comps.insert(comps.end(), labels.begin(), labels.end());
|
||||
|
||||
for(auto it = comps.begin(); it != comps.end(); it++)
|
||||
{
|
||||
GuiComponent* comp = *it;
|
||||
// an animation is playing
|
||||
// then animate if reverse != fadingOut
|
||||
// an animation is not playing
|
||||
// then animate if opacity != our target opacity
|
||||
if((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) ||
|
||||
(!comp->isAnimationPlaying(0) && comp->getOpacity() != (fadingOut ? 0 : 255)))
|
||||
{
|
||||
auto func = [comp](float t)
|
||||
{
|
||||
comp->setOpacity((unsigned char)(lerp<float>(0.0f, 1.0f, t)*255));
|
||||
};
|
||||
comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DetailedGameListView::launch(FileData* game)
|
||||
{
|
||||
Eigen::Vector3f target(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0);
|
||||
if(mImage.hasImage())
|
||||
target << mImage.getCenter().x(), mImage.getCenter().y(), 0;
|
||||
|
||||
ViewController::get()->launch(game, target);
|
||||
}
|
||||
|
||||
std::vector<TextComponent*> DetailedGameListView::getMDLabels()
|
||||
{
|
||||
std::vector<TextComponent*> ret;
|
||||
ret.push_back(&mLblRating);
|
||||
ret.push_back(&mLblReleaseDate);
|
||||
ret.push_back(&mLblDeveloper);
|
||||
ret.push_back(&mLblPublisher);
|
||||
ret.push_back(&mLblGenre);
|
||||
ret.push_back(&mLblPlayers);
|
||||
ret.push_back(&mLblLastPlayed);
|
||||
ret.push_back(&mLblPlayCount);
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<GuiComponent*> DetailedGameListView::getMDValues()
|
||||
{
|
||||
std::vector<GuiComponent*> ret;
|
||||
ret.push_back(&mRating);
|
||||
ret.push_back(&mReleaseDate);
|
||||
ret.push_back(&mDeveloper);
|
||||
ret.push_back(&mPublisher);
|
||||
ret.push_back(&mGenre);
|
||||
ret.push_back(&mPlayers);
|
||||
ret.push_back(&mLastPlayed);
|
||||
ret.push_back(&mPlayCount);
|
||||
return ret;
|
||||
}
|
44
es-app/src/views/gamelist/DetailedGameListView.h
Normal file
44
es-app/src/views/gamelist/DetailedGameListView.h
Normal file
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
|
||||
#include "views/gamelist/BasicGameListView.h"
|
||||
#include "components/ScrollableContainer.h"
|
||||
#include "components/RatingComponent.h"
|
||||
#include "components/DateTimeComponent.h"
|
||||
|
||||
class DetailedGameListView : public BasicGameListView
|
||||
{
|
||||
public:
|
||||
DetailedGameListView(Window* window, FileData* root);
|
||||
|
||||
virtual void onThemeChanged(const std::shared_ptr<ThemeData>& theme) override;
|
||||
|
||||
virtual const char* getName() const override { return "detailed"; }
|
||||
|
||||
protected:
|
||||
virtual void launch(FileData* game) override;
|
||||
|
||||
private:
|
||||
void updateInfoPanel();
|
||||
|
||||
void initMDLabels();
|
||||
void initMDValues();
|
||||
|
||||
ImageComponent mImage;
|
||||
|
||||
TextComponent mLblRating, mLblReleaseDate, mLblDeveloper, mLblPublisher, mLblGenre, mLblPlayers, mLblLastPlayed, mLblPlayCount;
|
||||
|
||||
RatingComponent mRating;
|
||||
DateTimeComponent mReleaseDate;
|
||||
TextComponent mDeveloper;
|
||||
TextComponent mPublisher;
|
||||
TextComponent mGenre;
|
||||
TextComponent mPlayers;
|
||||
DateTimeComponent mLastPlayed;
|
||||
TextComponent mPlayCount;
|
||||
|
||||
std::vector<TextComponent*> getMDLabels();
|
||||
std::vector<GuiComponent*> getMDValues();
|
||||
|
||||
ScrollableContainer mDescContainer;
|
||||
TextComponent mDescription;
|
||||
};
|
59
es-app/src/views/gamelist/GridGameListView.cpp
Normal file
59
es-app/src/views/gamelist/GridGameListView.cpp
Normal file
|
@ -0,0 +1,59 @@
|
|||
#include "views/gamelist/GridGameListView.h"
|
||||
#include "ThemeData.h"
|
||||
#include "Window.h"
|
||||
#include "views/ViewController.h"
|
||||
|
||||
GridGameListView::GridGameListView(Window* window, FileData* root) : ISimpleGameListView(window, root),
|
||||
mGrid(window)
|
||||
{
|
||||
mGrid.setPosition(0, mSize.y() * 0.2f);
|
||||
mGrid.setSize(mSize.x(), mSize.y() * 0.8f);
|
||||
addChild(&mGrid);
|
||||
|
||||
populateList(root->getChildren());
|
||||
}
|
||||
|
||||
FileData* GridGameListView::getCursor()
|
||||
{
|
||||
return mGrid.getSelected();
|
||||
}
|
||||
|
||||
void GridGameListView::setCursor(FileData* file)
|
||||
{
|
||||
if(!mGrid.setCursor(file))
|
||||
{
|
||||
populateList(file->getParent()->getChildren());
|
||||
mGrid.setCursor(file);
|
||||
}
|
||||
}
|
||||
|
||||
bool GridGameListView::input(InputConfig* config, Input input)
|
||||
{
|
||||
if(config->isMappedTo("left", input) || config->isMappedTo("right", input))
|
||||
return GuiComponent::input(config, input);
|
||||
|
||||
return ISimpleGameListView::input(config, input);
|
||||
}
|
||||
|
||||
void GridGameListView::populateList(const std::vector<FileData*>& files)
|
||||
{
|
||||
mGrid.clear();
|
||||
for(auto it = files.begin(); it != files.end(); it++)
|
||||
{
|
||||
mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it);
|
||||
}
|
||||
}
|
||||
|
||||
void GridGameListView::launch(FileData* game)
|
||||
{
|
||||
ViewController::get()->launch(game);
|
||||
}
|
||||
|
||||
std::vector<HelpPrompt> GridGameListView::getHelpPrompts()
|
||||
{
|
||||
std::vector<HelpPrompt> prompts;
|
||||
prompts.push_back(HelpPrompt("up/down/left/right", "scroll"));
|
||||
prompts.push_back(HelpPrompt("a", "launch"));
|
||||
prompts.push_back(HelpPrompt("b", "back"));
|
||||
return prompts;
|
||||
}
|
29
es-app/src/views/gamelist/GridGameListView.h
Normal file
29
es-app/src/views/gamelist/GridGameListView.h
Normal file
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include "views/gamelist/ISimpleGameListView.h"
|
||||
#include "components/ImageGridComponent.h"
|
||||
#include "components/ImageComponent.h"
|
||||
#include <stack>
|
||||
|
||||
class GridGameListView : public ISimpleGameListView
|
||||
{
|
||||
public:
|
||||
GridGameListView(Window* window, FileData* root);
|
||||
|
||||
//virtual void onThemeChanged(const std::shared_ptr<ThemeData>& theme) override;
|
||||
|
||||
virtual FileData* getCursor() override;
|
||||
virtual void setCursor(FileData*) override;
|
||||
|
||||
virtual bool input(InputConfig* config, Input input) override;
|
||||
|
||||
virtual const char* getName() const override { return "grid"; }
|
||||
|
||||
virtual std::vector<HelpPrompt> getHelpPrompts() override;
|
||||
|
||||
protected:
|
||||
virtual void populateList(const std::vector<FileData*>& files) override;
|
||||
virtual void launch(FileData* game) override;
|
||||
|
||||
ImageGridComponent<FileData*> mGrid;
|
||||
};
|
43
es-app/src/views/gamelist/IGameListView.cpp
Normal file
43
es-app/src/views/gamelist/IGameListView.cpp
Normal file
|
@ -0,0 +1,43 @@
|
|||
#include "views/gamelist/IGameListView.h"
|
||||
#include "Window.h"
|
||||
#include "guis/GuiMetaDataEd.h"
|
||||
#include "guis/GuiMenu.h"
|
||||
#include "guis/GuiGamelistOptions.h"
|
||||
#include "views/ViewController.h"
|
||||
#include "Settings.h"
|
||||
#include "Log.h"
|
||||
#include "Sound.h"
|
||||
|
||||
bool IGameListView::input(InputConfig* config, Input input)
|
||||
{
|
||||
// select to open GuiGamelistOptions
|
||||
if(config->isMappedTo("select", input) && input.value)
|
||||
{
|
||||
Sound::getFromTheme(mTheme, getName(), "menuOpen")->play();
|
||||
mWindow->pushGui(new GuiGamelistOptions(mWindow, this->mRoot->getSystem()));
|
||||
return true;
|
||||
|
||||
// Ctrl-R to reload a view when debugging
|
||||
}else if(Settings::getInstance()->getBool("Debug") && config->getDeviceId() == DEVICE_KEYBOARD &&
|
||||
(SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL)) && input.id == SDLK_r && input.value != 0)
|
||||
{
|
||||
LOG(LogDebug) << "reloading view";
|
||||
ViewController::get()->reloadGameListView(this, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return GuiComponent::input(config, input);
|
||||
}
|
||||
|
||||
void IGameListView::setTheme(const std::shared_ptr<ThemeData>& theme)
|
||||
{
|
||||
mTheme = theme;
|
||||
onThemeChanged(theme);
|
||||
}
|
||||
|
||||
HelpStyle IGameListView::getHelpStyle()
|
||||
{
|
||||
HelpStyle style;
|
||||
style.applyTheme(mTheme, getName());
|
||||
return style;
|
||||
}
|
42
es-app/src/views/gamelist/IGameListView.h
Normal file
42
es-app/src/views/gamelist/IGameListView.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
#include "FileData.h"
|
||||
#include "Renderer.h"
|
||||
|
||||
class Window;
|
||||
class GuiComponent;
|
||||
class FileData;
|
||||
class ThemeData;
|
||||
|
||||
// This is an interface that defines the minimum for a GameListView.
|
||||
class IGameListView : public GuiComponent
|
||||
{
|
||||
public:
|
||||
IGameListView(Window* window, FileData* root) : GuiComponent(window), mRoot(root)
|
||||
{ setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); }
|
||||
|
||||
virtual ~IGameListView() {}
|
||||
|
||||
// Called when a new file is added, a file is removed, a file's metadata changes, or a file's children are sorted.
|
||||
// NOTE: FILE_SORTED is only reported for the topmost FileData, where the sort started.
|
||||
// Since sorts are recursive, that FileData's children probably changed too.
|
||||
virtual void onFileChanged(FileData* file, FileChangeType change) = 0;
|
||||
|
||||
// Called whenever the theme changes.
|
||||
virtual void onThemeChanged(const std::shared_ptr<ThemeData>& theme) = 0;
|
||||
|
||||
void setTheme(const std::shared_ptr<ThemeData>& theme);
|
||||
inline const std::shared_ptr<ThemeData>& getTheme() const { return mTheme; }
|
||||
|
||||
virtual FileData* getCursor() = 0;
|
||||
virtual void setCursor(FileData*) = 0;
|
||||
|
||||
virtual bool input(InputConfig* config, Input input) override;
|
||||
|
||||
virtual const char* getName() const = 0;
|
||||
|
||||
virtual HelpStyle getHelpStyle() override;
|
||||
protected:
|
||||
FileData* mRoot;
|
||||
std::shared_ptr<ThemeData> mTheme;
|
||||
};
|
109
es-app/src/views/gamelist/ISimpleGameListView.cpp
Normal file
109
es-app/src/views/gamelist/ISimpleGameListView.cpp
Normal file
|
@ -0,0 +1,109 @@
|
|||
#include "views/gamelist/ISimpleGameListView.h"
|
||||
#include "ThemeData.h"
|
||||
#include "Window.h"
|
||||
#include "views/ViewController.h"
|
||||
#include "Sound.h"
|
||||
#include "Settings.h"
|
||||
|
||||
ISimpleGameListView::ISimpleGameListView(Window* window, FileData* root) : IGameListView(window, root),
|
||||
mHeaderText(window), mHeaderImage(window), mBackground(window), mThemeExtras(window)
|
||||
{
|
||||
mHeaderText.setText("Logo Text");
|
||||
mHeaderText.setSize(mSize.x(), 0);
|
||||
mHeaderText.setPosition(0, 0);
|
||||
mHeaderText.setAlignment(ALIGN_CENTER);
|
||||
|
||||
mHeaderImage.setResize(0, mSize.y() * 0.185f);
|
||||
mHeaderImage.setOrigin(0.5f, 0.0f);
|
||||
mHeaderImage.setPosition(mSize.x() / 2, 0);
|
||||
|
||||
mBackground.setResize(mSize.x(), mSize.y());
|
||||
|
||||
addChild(&mHeaderText);
|
||||
addChild(&mBackground);
|
||||
addChild(&mThemeExtras);
|
||||
}
|
||||
|
||||
void ISimpleGameListView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
|
||||
{
|
||||
using namespace ThemeFlags;
|
||||
mBackground.applyTheme(theme, getName(), "background", ALL);
|
||||
mHeaderImage.applyTheme(theme, getName(), "logo", ALL);
|
||||
mHeaderText.applyTheme(theme, getName(), "logoText", ALL);
|
||||
mThemeExtras.setExtras(ThemeData::makeExtras(theme, getName(), mWindow));
|
||||
|
||||
if(mHeaderImage.hasImage())
|
||||
{
|
||||
removeChild(&mHeaderText);
|
||||
addChild(&mHeaderImage);
|
||||
}else{
|
||||
addChild(&mHeaderText);
|
||||
removeChild(&mHeaderImage);
|
||||
}
|
||||
}
|
||||
|
||||
void ISimpleGameListView::onFileChanged(FileData* file, FileChangeType change)
|
||||
{
|
||||
// we could be tricky here to be efficient;
|
||||
// but this shouldn't happen very often so we'll just always repopulate
|
||||
FileData* cursor = getCursor();
|
||||
populateList(cursor->getParent()->getChildren());
|
||||
setCursor(cursor);
|
||||
}
|
||||
|
||||
bool ISimpleGameListView::input(InputConfig* config, Input input)
|
||||
{
|
||||
if(input.value != 0)
|
||||
{
|
||||
if(config->isMappedTo("a", input))
|
||||
{
|
||||
FileData* cursor = getCursor();
|
||||
if(cursor->getType() == GAME)
|
||||
{
|
||||
Sound::getFromTheme(getTheme(), getName(), "launch")->play();
|
||||
launch(cursor);
|
||||
}else{
|
||||
// it's a folder
|
||||
if(cursor->getChildren().size() > 0)
|
||||
{
|
||||
mCursorStack.push(cursor);
|
||||
populateList(cursor->getChildren());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}else if(config->isMappedTo("b", input))
|
||||
{
|
||||
if(mCursorStack.size())
|
||||
{
|
||||
populateList(mCursorStack.top()->getParent()->getChildren());
|
||||
setCursor(mCursorStack.top());
|
||||
mCursorStack.pop();
|
||||
Sound::getFromTheme(getTheme(), getName(), "back")->play();
|
||||
}else{
|
||||
onFocusLost();
|
||||
ViewController::get()->goToSystemView(getCursor()->getSystem());
|
||||
}
|
||||
|
||||
return true;
|
||||
}else if(config->isMappedTo("right", input))
|
||||
{
|
||||
if(Settings::getInstance()->getBool("QuickSystemSelect"))
|
||||
{
|
||||
onFocusLost();
|
||||
ViewController::get()->goToNextGameList();
|
||||
return true;
|
||||
}
|
||||
}else if(config->isMappedTo("left", input))
|
||||
{
|
||||
if(Settings::getInstance()->getBool("QuickSystemSelect"))
|
||||
{
|
||||
onFocusLost();
|
||||
ViewController::get()->goToPrevGameList();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return IGameListView::input(config, input);
|
||||
}
|
38
es-app/src/views/gamelist/ISimpleGameListView.h
Normal file
38
es-app/src/views/gamelist/ISimpleGameListView.h
Normal file
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
|
||||
#include "views/gamelist/IGameListView.h"
|
||||
|
||||
#include "components/TextComponent.h"
|
||||
#include "components/ImageComponent.h"
|
||||
|
||||
class ISimpleGameListView : public IGameListView
|
||||
{
|
||||
public:
|
||||
ISimpleGameListView(Window* window, FileData* root);
|
||||
virtual ~ISimpleGameListView() {}
|
||||
|
||||
// Called when a new file is added, a file is removed, a file's metadata changes, or a file's children are sorted.
|
||||
// NOTE: FILE_SORTED is only reported for the topmost FileData, where the sort started.
|
||||
// Since sorts are recursive, that FileData's children probably changed too.
|
||||
virtual void onFileChanged(FileData* file, FileChangeType change);
|
||||
|
||||
// Called whenever the theme changes.
|
||||
virtual void onThemeChanged(const std::shared_ptr<ThemeData>& theme);
|
||||
|
||||
virtual FileData* getCursor() = 0;
|
||||
virtual void setCursor(FileData*) = 0;
|
||||
|
||||
virtual bool input(InputConfig* config, Input input) override;
|
||||
|
||||
protected:
|
||||
virtual void populateList(const std::vector<FileData*>& files) = 0;
|
||||
virtual void launch(FileData* game) = 0;
|
||||
|
||||
TextComponent mHeaderText;
|
||||
ImageComponent mHeaderImage;
|
||||
ImageComponent mBackground;
|
||||
|
||||
ThemeExtras mThemeExtras;
|
||||
|
||||
std::stack<FileData*> mCursorStack;
|
||||
};
|
Loading…
Reference in a new issue