diff --git a/THEMES.md b/THEMES.md index 5399b03143..7ee2e04476 100644 --- a/THEMES.md +++ b/THEMES.md @@ -871,12 +871,17 @@ EmulationStation borrows the concept of "nine patches" from Android (or "9-Slice * `origin` - type: NORMALIZED_PAIR. - Where on the carousel `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the carousel exactly in the middle of the screen. If the "POSITION" and "SIZE" attributes are themable, "ORIGIN" is implied. * `color` - type: COLOR. - - Controls the color of the carousel background. - - Default is FFFFFFD8 + - Controls the color of the carousel background. + - Default is FFFFFFD8 +* `colorEnd` - type: COLOR. + - Optional end color when drawing a gradient background. + - Defaults to the same value as `color`. +* `gradientType` - type: STRING. + - Sets the gradient direction. Accepted values are "horizontal" (default) or "vertical". * `logoSize` - type: NORMALIZED_PAIR. Default is "0.25 0.155" * `logoScale` - type: FLOAT. - - Selected logo is increased in size by this scale - - Default is 1.2 + - Selected logo is increased in size by this scale + - Default is 1.2 * `logoRotation` - type: FLOAT. - Angle in degrees that the logos should be rotated. Value should be positive. - Default is 7.5 @@ -890,10 +895,16 @@ EmulationStation borrows the concept of "nine patches" from Android (or "9-Slice - Accepted values are "left", "right" or "center" when `type` is "vertical" or "vertical_wheel". - Default is "center" * `maxLogoCount` - type: FLOAT. - - Sets the number of logos to display in the carousel. - - Default is 3 + - Sets the number of logos to display in the carousel. + - Default is 3 +* `minLogoOpacity` - type: FLOAT. + - Sets a minimum opacity for unfocused logos (0.0 - 1.0). Default is 0.5. +* `scaledLogoSpacing` - type: FLOAT. + - Adds extra spacing when scaled logos overlap. Set to 0 for classic behaviour. Default is 0. * `zIndex` - type: FLOAT. - - z-index value for component. Components will be rendered in order of z-index value from low to high. + - z-index value for component. Components will be rendered in order of z-index value from low to high. +* `scrollSound` - type: PATH. + - Optional sound played while scrolling the system carousel. The help system is a special element that displays a context-sensitive list of actions the user can take at any time. You should try and keep the position constant throughout every screen. Keep in mind the "default" settings (including position) are used whenever the user opens a menu. diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index a828a462f3..7f9ebc0d4b 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -13,6 +13,12 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/FileFilterIndex.h ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemScreenSaver.h ${CMAKE_CURRENT_SOURCE_DIR}/src/CollectionSystemManager.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/LocaleES.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeOptions.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiThemeOptions.h + + # 🔊 Background music + ${CMAKE_CURRENT_SOURCE_DIR}/src/audio/BackgroundMusicManager.h # GuiComponents ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.h @@ -73,6 +79,15 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/FileFilterIndex.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemScreenSaver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CollectionSystemManager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/LocaleES.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeOptions.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiThemeOptions.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiThemeOptions.h + + + # 🔊 Background music + ${CMAKE_CURRENT_SOURCE_DIR}/src/audio/BackgroundMusicManager.cpp + # GuiComponents ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.cpp @@ -115,36 +130,25 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/views/UIModeController.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 +# 🔗 añadimos SDL2_mixer para la música de fondo +target_link_libraries(emulationstation ${COMMON_LIBRARIES} es-core SDL2_mixer) + if(MSVC) - # Always compile with the "WINDOWS" subsystem to avoid console window flashing at startup - # when --debug is not set (see es-core/src/main.cpp for explanation). - # The console will still be shown if launched with --debug. - # 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:WINDOWS") set_target_properties(emulationstation PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:WINDOWS") 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) @@ -154,7 +158,8 @@ 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") +# 🔹 CORRECCIÓN: se mueve el LICENSE.md a la raíz del proyecto +SET(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE.md") SET(CPACK_RESOURCE_FILE README "${CMAKE_CURRENT_SOURCE_DIR}/README.md") SET(CPACK_DEBIAN_PACKAGE_MAINTAINER "Alec Lofquist ") diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 440abf7fa8..379fce2631 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -13,29 +13,31 @@ #include "Settings.h" #include "SystemData.h" #include "ThemeData.h" +#include "LocaleES.h" + #include #include #include -/* Handling the getting, initialization, deinitialization, saving and deletion of - * a CollectionSystemManager Instance */ +// Handling the getting, initialization, deinitialization, saving and deletion of +// a CollectionSystemManager Instance CollectionSystemManager* CollectionSystemManager::sInstance = NULL; CollectionSystemManager::CollectionSystemManager(Window* window) : mWindow(window) { CollectionSystemDecl systemDecls[] = { - //type name long name (display) default sort (key, order) theme folder isCustom - { AUTO_ALL_GAMES, "all", "all games", "name, ascending", "auto-allgames", false }, - { AUTO_LAST_PLAYED, "recent", "last played", "last played, descending", "auto-lastplayed", false }, - { AUTO_FAVORITES, "favorites", "favorites", "name, ascending", "auto-favorites", false }, - { AUTO_RANDOM, RANDOM_COLL_ID, "random", "name, ascending", "auto-random", false }, - { CUSTOM_COLLECTION, CUSTOM_COLL_ID, "collections", "name, ascending", "custom-collections", true } + // type name long name (display) default sort (key, order) theme folder isCustom + { AUTO_ALL_GAMES, "all", "ALL GAMES", "name, ascending", "auto-allgames", false }, + { AUTO_LAST_PLAYED, "recent", "LAST PLAYED", "last played, descending", "auto-lastplayed", false }, + { AUTO_FAVORITES, "favorites", "FAVORITES", "name, ascending", "auto-favorites", false }, + { AUTO_RANDOM, RANDOM_COLL_ID, "RANDOM", "name, ascending", "auto-random", false }, + { CUSTOM_COLLECTION, CUSTOM_COLL_ID, "COLLECTIONS", "name, ascending", "custom-collections", true } }; // create a map std::vector tempSystemDecl = std::vector(systemDecls, systemDecls + sizeof(systemDecls) / sizeof(systemDecls[0])); - for (std::vector::const_iterator it = tempSystemDecl.cbegin(); it != tempSystemDecl.cend(); ++it ) + for (std::vector::const_iterator it = tempSystemDecl.cbegin(); it != tempSystemDecl.cend(); ++it) { mCollectionSystemDeclsIndex[(*it).name] = (*it); } @@ -51,7 +53,7 @@ CollectionSystemManager::CollectionSystemManager(Window* window) : mWindow(windo mCollectionEnvData->mPlatformIds = allPlatformIds; std::string path = getCollectionsFolder(); - if(!Utils::FileSystem::exists(path)) + if (!Utils::FileSystem::exists(path)) Utils::FileSystem::createDirectory(path); mIsEditingCustom = false; @@ -67,7 +69,7 @@ CollectionSystemManager::~CollectionSystemManager() removeCollectionsFromDisplayedSystems(); // iterate the map - for(std::map::const_iterator it = mCustomCollectionSystemsData.cbegin() ; it != mCustomCollectionSystemsData.cend() ; it++ ) + for (std::map::const_iterator it = mCustomCollectionSystemsData.cbegin(); it != mCustomCollectionSystemsData.cend(); it++) { if (it->second.isPopulated) { @@ -118,12 +120,12 @@ bool CollectionSystemManager::saveCustomCollection(SystemData* sys) if (!configFile.good()) { auto const errNo = errno; - LOG(LogError) << "Failed to create file, collection not created: " << absCollectionFn << ": " << std::strerror(errNo) << " (" << errNo << ")"; + LOG(LogError) << "Failed to create file, collection not created: " << absCollectionFn << ": " << std::strerror(errNo) << " (" << errNo << ")"; return false; } - for(std::unordered_map::const_iterator iter = games.cbegin(); iter != games.cend(); ++iter) + for (std::unordered_map::const_iterator iter = games.cbegin(); iter != games.cend(); ++iter) { - std::string path = iter->first; + std::string path = iter->first; configFile << path << std::endl; } configFile.close(); @@ -140,7 +142,7 @@ void CollectionSystemManager::loadCollectionSystems(bool async) mCustomCollectionsBundle = createNewCollectionEntry(decl.name, decl, CollectionFlags::NONE); // we will also load custom systems here initCustomCollectionSystems(); - if(Settings::getInstance()->getString("CollectionSystemsAuto") != "" || Settings::getInstance()->getString("CollectionSystemsCustom") != "") + if (Settings::getInstance()->getString("CollectionSystemsAuto") != "" || Settings::getInstance()->getString("CollectionSystemsCustom") != "") { // Now see which ones are enabled loadEnabledListFromSettings(); @@ -158,7 +160,7 @@ void CollectionSystemManager::loadEnabledListFromSettings() std::vector autoSelected = Utils::String::delimitedStringToVector(Settings::getInstance()->getString("CollectionSystemsAuto"), ",", true); // iterate the map - for(std::map::iterator it = mAutoCollectionSystemsData.begin() ; it != mAutoCollectionSystemsData.end() ; it++ ) + for (std::map::iterator it = mAutoCollectionSystemsData.begin(); it != mAutoCollectionSystemsData.end(); it++) { it->second.isEnabled = (std::find(autoSelected.cbegin(), autoSelected.cend(), it->first) != autoSelected.cend()); } @@ -167,7 +169,7 @@ void CollectionSystemManager::loadEnabledListFromSettings() std::vector customSelected = Utils::String::delimitedStringToVector(Settings::getInstance()->getString("CollectionSystemsCustom"), ",", true); // iterate the map - for(std::map::iterator it = mCustomCollectionSystemsData.begin() ; it != mCustomCollectionSystemsData.end() ; it++ ) + for (std::map::iterator it = mCustomCollectionSystemsData.begin(); it != mCustomCollectionSystemsData.end(); it++) { it->second.isEnabled = (std::find(customSelected.cbegin(), customSelected.cend(), it->first) != customSelected.cend()); } @@ -181,13 +183,13 @@ void CollectionSystemManager::updateSystemsList() // add custom enabled ones addEnabledCollectionsToDisplayedSystems(&mCustomCollectionSystemsData, false); - if(Settings::getInstance()->getBool("SortAllSystems")) + if (Settings::getInstance()->getBool("SortAllSystems")) { // sort custom individual systems with other systems std::sort(SystemData::sSystemVector.begin(), SystemData::sSystemVector.end(), systemSort); // move RetroPie system to end, before auto collections - for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); ) + for (auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); ) { if ((*sysIt)->getName() == "retropie") { @@ -203,7 +205,7 @@ void CollectionSystemManager::updateSystemsList() } } - if(mCustomCollectionsBundle->getRootFolder()->getChildren().size() > 0) + if (mCustomCollectionsBundle->getRootFolder()->getChildren().size() > 0) { mCustomCollectionsBundle->getRootFolder()->sort(getSortTypeFromString(mCollectionSystemDeclsIndex[CUSTOM_COLL_ID].defaultSort)); SystemData::sSystemVector.push_back(mCustomCollectionsBundle); @@ -215,14 +217,14 @@ void CollectionSystemManager::updateSystemsList() addEnabledCollectionsToDisplayedSystems(&mAutoCollectionSystemsData, true); // create views for collections, before reload - for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) + for (auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) { if ((*sysIt)->isCollection()) ViewController::get()->getGameListView((*sysIt)); } // if we were editing a custom collection, and it's no longer enabled, exit edit mode - if(mIsEditingCustom && !mEditingCollectionSystemData->isEnabled) + if (mIsEditingCustom && !mEditingCollectionSystemData->isEnabled) { exitEditMode(); } @@ -239,7 +241,7 @@ void CollectionSystemManager::refreshCollectionSystems(FileData* file) allCollections.insert(mAutoCollectionSystemsData.cbegin(), mAutoCollectionSystemsData.cend()); allCollections.insert(mCustomCollectionSystemsData.cbegin(), mCustomCollectionSystemsData.cend()); - for(auto sysDataIt = allCollections.cbegin(); sysDataIt != allCollections.cend(); sysDataIt++) + for (auto sysDataIt = allCollections.cbegin(); sysDataIt != allCollections.cend(); sysDataIt++) { updateCollectionSystem(file, sysDataIt->second); } @@ -327,7 +329,7 @@ void CollectionSystemManager::deleteCollectionFiles(FileData* file) allCollections.insert(mAutoCollectionSystemsData.cbegin(), mAutoCollectionSystemsData.cend()); allCollections.insert(mCustomCollectionSystemsData.cbegin(), mCustomCollectionSystemsData.cend()); - for(auto sysDataIt = allCollections.begin(); sysDataIt != allCollections.end(); sysDataIt++) + for (auto sysDataIt = allCollections.begin(); sysDataIt != allCollections.end(); sysDataIt++) { if (sysDataIt->second.isPopulated) { @@ -348,9 +350,9 @@ void CollectionSystemManager::deleteCollectionFiles(FileData* file) bool CollectionSystemManager::isThemeGenericCollectionCompatible(bool genericCustomCollections) { std::vector cfgSys = getCollectionThemeFolders(genericCustomCollections); - for(auto sysIt = cfgSys.cbegin(); sysIt != cfgSys.cend(); sysIt++) + for (auto sysIt = cfgSys.cbegin(); sysIt != cfgSys.cend(); sysIt++) { - if(!themeFolderExists(*sysIt)) + if (!themeFolderExists(*sysIt)) return false; } return true; @@ -364,7 +366,7 @@ bool CollectionSystemManager::isThemeCustomCollectionCompatible(std::vectorgetString("ThemeSet")); - if(set != themeSets.cend()) + if (set != themeSets.cend()) { std::string defaultThemeFilePath = set->second.path + "/theme.xml"; if (Utils::FileSystem::exists(defaultThemeFilePath)) @@ -373,9 +375,9 @@ bool CollectionSystemManager::isThemeCustomCollectionCompatible(std::vector 0) { + if (index > 0) { name = name.substr(0, name.size() - infix.size()); } - return getValidNewCollectionName(name, index+1); + return getValidNewCollectionName(name, index + 1); } } // if it matches one of the custom collections reserved names if (mCollectionSystemDeclsIndex.find(name) != mCollectionSystemDeclsIndex.cend()) - return getValidNewCollectionName(name, index+1); + return getValidNewCollectionName(name, index + 1); return name; } @@ -458,7 +460,19 @@ void CollectionSystemManager::setEditMode(std::string collectionName, bool quiet mEditingCollectionSystemData = sysData; if (!quiet) { - GuiInfoPopup* s = new GuiInfoPopup(mWindow, "Editing the '" + Utils::String::toUpper(collectionName) + "' Collection. Add/remove games with Y.", 8000); + std::string colUpper = Utils::String::toUpper(collectionName); + + std::string editingBase = LocaleES::get("EDITING_COLLECTION"); + if (editingBase == "EDITING_COLLECTION") + editingBase = "Editing the collection"; + + std::string addRemove = LocaleES::get("ADD_REMOVE_GAMES_Y"); + if (addRemove == "ADD_REMOVE_GAMES_Y") + addRemove = "Add/remove games with Y."; + + std::string msg = editingBase + " '" + colUpper + "'. " + addRemove; + + GuiInfoPopup* s = new GuiInfoPopup(mWindow, msg, 8000); mWindow->setInfoPopup(s); } } @@ -466,7 +480,12 @@ void CollectionSystemManager::setEditMode(std::string collectionName, bool quiet void CollectionSystemManager::exitEditMode(bool quiet) { if (!quiet) { - GuiInfoPopup* s = new GuiInfoPopup(mWindow, "Finished editing the '" + Utils::String::toUpper(mEditingCollection) + "' Collection.", 4000); + std::string finishedBase = LocaleES::get("FINISHED_EDITING_COLLECTION"); + if (finishedBase == "FINISHED_EDITING_COLLECTION") + finishedBase = "Finished editing the collection"; + + std::string msg = finishedBase + " '" + Utils::String::toUpper(mEditingCollection) + "'."; + GuiInfoPopup* s = new GuiInfoPopup(mWindow, msg, 4000); mWindow->setInfoPopup(s); } if (mIsEditingCustom) { @@ -481,7 +500,8 @@ int CollectionSystemManager::getPressCountInDuration() { Uint32 now = SDL_GetTicks(); if (now - mFirstPressMs < DOUBLE_PRESS_DETECTION_DURATION) { return 2; - } else { + } + else { mFirstPressMs = now; return 1; } @@ -523,7 +543,7 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) // remove from index fileIndex->removeFromIndex(collectionEntry); // remove from bundle index as well, if needed - if(systemViewToUpdate != sysData) + if (systemViewToUpdate != sysData) { systemViewToUpdate->getIndex()->removeFromIndex(collectionEntry); } @@ -543,7 +563,7 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) rootFolder->sort(getSortTypeFromString(mEditingCollectionSystemData->decl.defaultSort)); ViewController::get()->onFileChanged(systemViewToUpdate->getRootFolder(), FILE_SORTED); // add to bundle index as well, if needed - if(systemViewToUpdate != sysData) + if (systemViewToUpdate != sysData) { systemViewToUpdate->getIndex()->addToIndex(newGame); } @@ -574,13 +594,40 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) refreshCollectionSystems(file->getSourceFileData()); } + + std::string sysNameUpper = Utils::String::toUpper(sysName); + if (adding) { - s = new GuiInfoPopup(mWindow, "Added '" + Utils::String::removeParenthesis(name) + "' to '" + Utils::String::toUpper(sysName) + "'", 4000); + std::string addedBase = LocaleES::get("ADDED_TO_COLLECTION"); + if (addedBase == "ADDED_TO_COLLECTION") + addedBase = "Added"; + + std::string toWord = LocaleES::get("TO_COLLECTION"); + if (toWord == "TO_COLLECTION") + toWord = "to"; + + s = new GuiInfoPopup( + mWindow, + addedBase + " '" + Utils::String::removeParenthesis(name) + "' " + toWord + " '" + sysNameUpper + "'", + 4000 + ); } else { - s = new GuiInfoPopup(mWindow, "Removed '" + Utils::String::removeParenthesis(name) + "' from '" + Utils::String::toUpper(sysName) + "'", 4000); + std::string removedBase = LocaleES::get("REMOVED_FROM_COLLECTION"); + if (removedBase == "REMOVED_FROM_COLLECTION") + removedBase = "Removed"; + + std::string fromWord = LocaleES::get("FROM_COLLECTION"); + if (fromWord == "FROM_COLLECTION") + fromWord = "from"; + + s = new GuiInfoPopup( + mWindow, + removedBase + " '" + Utils::String::removeParenthesis(name) + "' " + fromWord + " '" + sysNameUpper + "'", + 4000 + ); } mWindow->setInfoPopup(s); @@ -638,7 +685,7 @@ void CollectionSystemManager::recreateCollection(SystemData* sysData) SystemData* systemViewToUpdate = getSystemToView(sysData); // while there are games there, remove them from the view and system - while(rootFolder->getChildrenByFilename().size() > 0) + while (rootFolder->getChildrenByFilename().size() > 0) ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(rootFolder->getChildrenByFilename().begin()->second, false, false); colSysData->isPopulated = false; @@ -661,7 +708,7 @@ void CollectionSystemManager::recreateCollection(SystemData* sysData) // loads Automatic Collection systems (All, Favorites, Last Played, Random) void CollectionSystemManager::initAutoCollectionSystems() { - for(std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin() ; it != mCollectionSystemDeclsIndex.cend() ; it++ ) + for (std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin(); it != mCollectionSystemDeclsIndex.cend(); it++) { CollectionSystemDecl sysDecl = it->second; if (!sysDecl.isCustom) @@ -679,7 +726,10 @@ void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) { FileData* rootFolder = sys->getRootFolder(); - std::string desc = "This collection is empty."; + std::string desc = LocaleES::get("EMPTY_COLLECTION"); + if (desc == "EMPTY_COLLECTION") + desc = "This collection is empty."; + std::string rating = "0"; std::string players = "1"; std::string releasedate = "N/A"; @@ -691,11 +741,11 @@ void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) std::unordered_map games = rootFolder->getChildrenByFilename(); - if(games.size() > 0) + if (games.size() > 0) { std::string games_list = ""; int games_counter = 0; - for(std::unordered_map::const_iterator iter = games.cbegin(); iter != games.cend(); ++iter) + for (std::unordered_map::const_iterator iter = games.cbegin(); iter != games.cend(); ++iter) { games_counter++; FileData* file = iter->second; @@ -712,21 +762,21 @@ void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) developer = (developer == "None" ? new_developer : (new_developer != developer ? "Various" : new_developer)); genre = (genre == "None" ? new_genre : (new_genre != genre ? "Various" : new_genre)); - switch(games_counter) + switch (games_counter) { - case 2: - case 3: - games_list += ", "; - case 1: - games_list += "'" + file->getName() + "'"; - break; - case 4: - games_list += " among other titles."; + case 2: + case 3: + games_list += ", "; + case 1: + games_list += "'" + file->getName() + "'"; + break; + case 4: + games_list += " among other titles."; } } desc = "This collection contains " + std::to_string(games_counter) + " game" - + (games_counter == 1 ? "" : "s") + ", including " + games_list; + + (games_counter == 1 ? "" : "s") + ", including " + games_list; FileData* randomGame = sys->getRandomGame(); @@ -781,7 +831,27 @@ SystemData* CollectionSystemManager::addNewCustomCollection(std::string name, bo // creates a new, empty Collection system, based on the name and declaration SystemData* CollectionSystemManager::createNewCollectionEntry(std::string name, CollectionSystemDecl sysDecl, const CollectionFlags flags) { - SystemData* newSys = new SystemData(name, sysDecl.longName, mCollectionEnvData, sysDecl.themeFolder, true); + // longName en este punto es: + // - "ALL GAMES" + // - "LAST PLAYED" + // - "FAVORITES" + // - "RANDOM" + // - "COLLECTIONS" + std::string displayName = sysDecl.longName; + + if (!sysDecl.isCustom) + { + // Usamos la misma clave en MAYÚSCULAS para el .ini + std::string key = sysDecl.longName; + std::string translated = LocaleES::get(key); + + if (translated != key) + displayName = translated; + else + displayName = sysDecl.longName; + } + + SystemData* newSys = new SystemData(name, displayName, mCollectionEnvData, sysDecl.themeFolder, true); CollectionSystemData newCollectionData; newCollectionData.system = newSys; @@ -824,7 +894,7 @@ void CollectionSystemManager::addRandomGames(SystemData* newSys, SystemData* sou } // load exclusion collection - std::unordered_map exclusionMap; + std::unordered_map exclusionMap; std::string exclusionCollection = Settings::getInstance()->getString("RandomCollectionExclusionCollection"); auto sysDataIt = mCustomCollectionSystemsData.find(exclusionCollection); @@ -850,7 +920,7 @@ void CollectionSystemManager::addRandomGames(SystemData* newSys, SystemData* sou FileData* randomGame = sourceSystem->getRandomGame()->getSourceFileData(); CollectionFileData* newGame = NULL; - if(exclusionMap.find(randomGame->getFullPath()) == exclusionMap.end()) + if (exclusionMap.find(randomGame->getFullPath()) == exclusionMap.end()) { // Not in the exclusion collection newGame = new CollectionFileData(randomGame, newSys); @@ -889,7 +959,7 @@ void CollectionSystemManager::populateRandomCollectionFromCollections(std::mapgetIndex(); // iterate the auto collections map - for(auto &c : mAutoCollectionSystemsData) + for (auto& c : mAutoCollectionSystemsData) { CollectionSystemData csd = c.second; // we can't add games from the random collection to the random collection @@ -905,7 +975,7 @@ void CollectionSystemManager::populateRandomCollectionFromCollections(std::mapisGameSystem() && !(*sysIt)->isCollection()) @@ -950,24 +1020,24 @@ void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysDa { std::vector files = (*sysIt)->getRootFolder()->getFilesRecursive(GAME); - for(auto gameIt = files.cbegin(); gameIt != files.cend(); gameIt++) + for (auto gameIt = files.cbegin(); gameIt != files.cend(); gameIt++) { bool include = includeFileInAutoCollections(*gameIt); - switch(sysDecl.type) { - case AUTO_LAST_PLAYED: - include = include && (*gameIt)->metadata.get("playcount") > "0"; - break; - case AUTO_FAVORITES: - // we may still want to add files we don't want in auto collections in "favorites" - include = (*gameIt)->metadata.get("favorite") == "true"; - break; - case AUTO_ALL_GAMES: - break; - default: - // No-op to prevent compiler warnings - // Getting here means that the file is not part of a pre-defined collection. - include = false; - break; + switch (sysDecl.type) { + case AUTO_LAST_PLAYED: + include = include && (*gameIt)->metadata.get("playcount") > "0"; + break; + case AUTO_FAVORITES: + // we may still want to add files we don't want in auto collections in "favorites" + include = (*gameIt)->metadata.get("favorite") == "true"; + break; + case AUTO_ALL_GAMES: + break; + default: + // No-op to prevent compiler warnings + // Getting here means that the file is not part of a pre-defined collection. + include = false; + break; } if (include) @@ -1008,7 +1078,7 @@ void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sys CollectionSystemDecl sysDecl = sysData->decl; std::string path = getCustomCollectionConfigPath(newSys->getName()); - if(!Utils::FileSystem::exists(path)) + if (!Utils::FileSystem::exists(path)) { LOG(LogInfo) << "Couldn't find custom collection config file at " << path; return; @@ -1022,12 +1092,12 @@ void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sys std::ifstream input(path); // get all files map - std::unordered_map allFilesMap = getAllGamesCollection()->getRootFolder()->getChildrenByFilename(); + std::unordered_map allFilesMap = getAllGamesCollection()->getRootFolder()->getChildrenByFilename(); // iterate list of files in config file - for(std::string gameKey; getline(input, gameKey); ) + for (std::string gameKey; getline(input, gameKey); ) { - std::unordered_map::const_iterator it = allFilesMap.find(gameKey); + std::unordered_map::const_iterator it = allFilesMap.find(gameKey); if (it != allFilesMap.cend()) { CollectionFileData* newGame = new CollectionFileData(it->second, newSys); @@ -1048,7 +1118,7 @@ void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sys void CollectionSystemManager::removeCollectionsFromDisplayedSystems() { // remove all Collection Systems - for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); ) + for (auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); ) { if ((*sysIt)->isCollection()) { @@ -1066,7 +1136,7 @@ void CollectionSystemManager::removeCollectionsFromDisplayedSystems() { FileData* customRoot = mCustomCollectionsBundle->getRootFolder(); std::vector mChildren = customRoot->getChildren(); - for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) { customRoot->removeChild(*it); } @@ -1081,24 +1151,24 @@ void CollectionSystemManager::removeCollectionsFromDisplayedSystems() void CollectionSystemManager::addEnabledCollectionsToDisplayedSystems(std::map* colSystemData, bool processRandom) { // add auto enabled ones - for(std::map::iterator it = colSystemData->begin() ; it != colSystemData->end() ; it++ ) + for (std::map::iterator it = colSystemData->begin(); it != colSystemData->end(); it++) { if ((!processRandom && it->second.decl.type != AUTO_RANDOM) || (processRandom && it->second.decl.type == AUTO_RANDOM)) { - if(it->second.isEnabled) + if (it->second.isEnabled) { // check if populated, otherwise populate if (!it->second.isPopulated) { - if(it->second.decl.isCustom) + if (it->second.decl.isCustom) populateCustomCollection(&(it->second)); else populateAutoCollection(&(it->second)); } // check if it has its own view - if(!it->second.decl.isCustom || themeFolderExists(it->first) || !Settings::getInstance()->getBool("UseCustomCollectionsSystem")) + if (!it->second.decl.isCustom || themeFolderExists(it->first) || !Settings::getInstance()->getBool("UseCustomCollectionsSystem")) { // exists theme folder, or we chose not to bundle it under the custom-collections system // so we need to create a view @@ -1121,7 +1191,7 @@ std::vector CollectionSystemManager::getSystemsFromConfig() std::vector systems; std::string path = SystemData::getConfigPath(false); - if(!Utils::FileSystem::exists(path)) + if (!Utils::FileSystem::exists(path)) { return systems; } @@ -1129,7 +1199,7 @@ std::vector CollectionSystemManager::getSystemsFromConfig() pugi::xml_document doc; pugi::xml_parse_result res = doc.load_file(path.c_str()); - if(!res) + if (!res) { return systems; } @@ -1137,12 +1207,12 @@ std::vector CollectionSystemManager::getSystemsFromConfig() //actually read the file pugi::xml_node systemList = doc.child("systemList"); - if(!systemList) + if (!systemList) { return systems; } - for(pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) { // theme folder std::string themeFolder = system.child("theme").text().get(); @@ -1158,14 +1228,14 @@ std::vector CollectionSystemManager::getSystemsFromTheme() std::vector systems; auto themeSets = ThemeData::getThemeSets(); - if(themeSets.empty()) + if (themeSets.empty()) { // no theme sets available return systems; } std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(set == themeSets.cend()) + if (set == themeSets.cend()) { // currently selected theme set is missing, so just pick the first available set set = themeSets.cbegin(); @@ -1184,9 +1254,9 @@ std::vector CollectionSystemManager::getSystemsFromTheme() { //... here you have a directory std::string folder = *it; - folder = folder.substr(themePath.size()+1); + folder = folder.substr(themePath.size() + 1); - if(Utils::FileSystem::exists(set->second.getThemePath(folder))) + if (Utils::FileSystem::exists(set->second.getThemePath(folder))) { systems.push_back(folder); } @@ -1215,7 +1285,7 @@ std::vector CollectionSystemManager::getUnusedSystemsFromTheme() systemsInUse.insert(systemsInUse.cend(), customSys.cbegin(), customSys.cend()); systemsInUse.insert(systemsInUse.cend(), userSys.cbegin(), userSys.cend()); - for(auto sysIt = themeSys.cbegin(); sysIt != themeSys.cend(); ) + for (auto sysIt = themeSys.cbegin(); sysIt != themeSys.cend(); ) { if (std::find(systemsInUse.cbegin(), systemsInUse.cend(), *sysIt) != systemsInUse.cend()) { @@ -1248,7 +1318,7 @@ std::vector CollectionSystemManager::getCollectionsFromConfigFolder // need to confirm filename matches config format if (filename != "custom-.cfg" && Utils::String::startsWith(filename, "custom-") && Utils::String::endsWith(filename, ".cfg")) { - filename = filename.substr(7, filename.size()-11); + filename = filename.substr(7, filename.size() - 11); systems.push_back(filename); } else @@ -1265,7 +1335,7 @@ std::vector CollectionSystemManager::getCollectionsFromConfigFolder std::vector CollectionSystemManager::getCollectionThemeFolders(bool custom) { std::vector systems; - for(std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin() ; it != mCollectionSystemDeclsIndex.cend() ; it++ ) + for (std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin(); it != mCollectionSystemDeclsIndex.cend(); it++) { CollectionSystemDecl sysDecl = it->second; if (sysDecl.isCustom == custom) @@ -1280,7 +1350,7 @@ std::vector CollectionSystemManager::getCollectionThemeFolders(bool std::vector CollectionSystemManager::getUserCollectionThemeFolders() { std::vector systems; - for(std::map::const_iterator it = mCustomCollectionSystemsData.cbegin() ; it != mCustomCollectionSystemsData.cend() ; it++ ) + for (std::map::const_iterator it = mCustomCollectionSystemsData.cbegin(); it != mCustomCollectionSystemsData.cend(); it++) { systems.push_back(it->second.decl.themeFolder); } @@ -1306,8 +1376,14 @@ bool CollectionSystemManager::includeFileInAutoCollections(FileData* file) bool CollectionSystemManager::needDoublePress(int presscount) { if (Settings::getInstance()->getBool("DoublePressRemovesFromFavs") && presscount < 2) { - GuiInfoPopup* toast = new GuiInfoPopup(mWindow, "Press again to remove from '" + Utils::String::toUpper(mEditingCollection) - + "'", DOUBLE_PRESS_DETECTION_DURATION, 100, 200); + std::string base = LocaleES::get("PRESS_AGAIN_TO_REMOVE"); + if (base == "PRESS_AGAIN_TO_REMOVE") + base = "Press again to remove from"; + + std::string msg = base + " '" + Utils::String::toUpper(mEditingCollection) + "'"; + + GuiInfoPopup* toast = new GuiInfoPopup(mWindow, msg, + DOUBLE_PRESS_DETECTION_DURATION, 100, 200); mWindow->setInfoPopup(toast); return true; } diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index 7f9faa68e8..fd0f9d54cc 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -3,40 +3,51 @@ #include "utils/StringUtil.h" #include "Settings.h" #include "Log.h" +#include "LocaleESHook.h" // 🔹 para es_translate() namespace FileSorts { const FileData::SortType typesArr[] = { - FileData::SortType(&compareName, true, "name, ascending"), - FileData::SortType(&compareName, false, "name, descending"), + // Nombre + FileData::SortType(&compareName, true, es_translate("name, ascending")), + FileData::SortType(&compareName, false, es_translate("name, descending")), - FileData::SortType(&compareRating, true, "rating, ascending"), - FileData::SortType(&compareRating, false, "rating, descending"), + // Rating + FileData::SortType(&compareRating, true, es_translate("rating, ascending")), + FileData::SortType(&compareRating, false, es_translate("rating, descending")), - FileData::SortType(&compareTimesPlayed, true, "times played, ascending"), - FileData::SortType(&compareTimesPlayed, false, "times played, descending"), + // Veces jugado + FileData::SortType(&compareTimesPlayed, true, es_translate("times played, ascending")), + FileData::SortType(&compareTimesPlayed, false, es_translate("times played, descending")), - FileData::SortType(&compareLastPlayed, true, "last played, ascending"), - FileData::SortType(&compareLastPlayed, false, "last played, descending"), + // Última vez jugado + FileData::SortType(&compareLastPlayed, true, es_translate("last played, ascending")), + FileData::SortType(&compareLastPlayed, false, es_translate("last played, descending")), - FileData::SortType(&compareNumPlayers, true, "number players, ascending"), - FileData::SortType(&compareNumPlayers, false, "number players, descending"), + // Jugadores + FileData::SortType(&compareNumPlayers, true, es_translate("number players, ascending")), + FileData::SortType(&compareNumPlayers, false, es_translate("number players, descending")), - FileData::SortType(&compareReleaseDate, true, "release date, ascending"), - FileData::SortType(&compareReleaseDate, false, "release date, descending"), + // Fecha de lanzamiento + FileData::SortType(&compareReleaseDate, true, es_translate("release date, ascending")), + FileData::SortType(&compareReleaseDate, false, es_translate("release date, descending")), - FileData::SortType(&compareGenre, true, "genre, ascending"), - FileData::SortType(&compareGenre, false, "genre, descending"), + // Género + FileData::SortType(&compareGenre, true, es_translate("genre, ascending")), + FileData::SortType(&compareGenre, false, es_translate("genre, descending")), - FileData::SortType(&compareDeveloper, true, "developer, ascending"), - FileData::SortType(&compareDeveloper, false, "developer, descending"), + // Desarrollador + FileData::SortType(&compareDeveloper, true, es_translate("developer, ascending")), + FileData::SortType(&compareDeveloper, false, es_translate("developer, descending")), - FileData::SortType(&comparePublisher, true, "publisher, ascending"), - FileData::SortType(&comparePublisher, false, "publisher, descending"), + // Distribuidor + FileData::SortType(&comparePublisher, true, es_translate("publisher, ascending")), + FileData::SortType(&comparePublisher, false, es_translate("publisher, descending")), - FileData::SortType(&compareSystem, true, "system, ascending"), - FileData::SortType(&compareSystem, false, "system, descending") + // Sistema + FileData::SortType(&compareSystem, true, es_translate("system, ascending")), + FileData::SortType(&compareSystem, false, es_translate("system, descending")) }; const std::vector SortTypes(typesArr, typesArr + sizeof(typesArr)/sizeof(typesArr[0])); @@ -123,7 +134,7 @@ namespace FileSorts } //If option is enabled, ignore leading articles by temporarily modifying the name prior to sorting - //(Artciles are defined within the settings config file) + //(Articles are defined within the settings config file) void ignoreLeadingArticles(std::string &name1, std::string &name2) { if (Settings::getInstance()->getBool("IgnoreLeadingArticles")) @@ -133,7 +144,6 @@ namespace FileSorts for(Utils::String::stringVector::iterator it = articles.begin(); it != articles.end(); it++) { - if (Utils::String::startsWith(Utils::String::toUpper(name1), Utils::String::toUpper(it[0]) + " ")) { name1 = Utils::String::replace(Utils::String::toUpper(name1), Utils::String::toUpper(it[0]) + " ", ""); } @@ -141,11 +151,8 @@ namespace FileSorts if (Utils::String::startsWith(Utils::String::toUpper(name2), Utils::String::toUpper(it[0]) + " ")) { name2 = Utils::String::replace(Utils::String::toUpper(name2), Utils::String::toUpper(it[0]) + " ", ""); } - } - } - } }; diff --git a/es-app/src/LocaleES.cpp b/es-app/src/LocaleES.cpp new file mode 100644 index 0000000000..3880ce9011 --- /dev/null +++ b/es-app/src/LocaleES.cpp @@ -0,0 +1,136 @@ +#include "LocaleES.h" +#include "Settings.h" +#include "utils/FileSystemUtil.h" +#include "Log.h" + +#include +#include +#include + +// -------------------- +// Singleton +// -------------------- +LocaleES& LocaleES::getInstance() +{ + static LocaleES instance; + return instance; +} + +LocaleES::LocaleES() + : mCurrentLanguage("en") +{ +} + +// -------------------- +// Lectura de archivos .ini +// -------------------- +bool LocaleES::loadLanguageFile(const std::string& filePath) +{ + std::ifstream in(filePath); + if (!in.is_open()) + { + LOG(LogWarning) << "LocaleES: could not open language file: " << filePath; + return false; + } + + mTranslations.clear(); + + std::string line; + + auto trim = [](std::string& s) + { + // recorta espacios al inicio y al final + size_t start = 0; + while (start < s.size() && (s[start] == ' ' || s[start] == '\t' || s[start] == '\r' || s[start] == '\n')) + ++start; + + size_t end = s.size(); + while (end > start && (s[end - 1] == ' ' || s[end - 1] == '\t' || s[end - 1] == '\r' || s[end - 1] == '\n')) + --end; + + s = s.substr(start, end - start); + }; + + while (std::getline(in, line)) + { + trim(line); + + // Saltar líneas vacías, comentarios o secciones [XXX] + if (line.empty() || line[0] == '#' || line[0] == ';' || line[0] == '[') + continue; + + auto pos = line.find('='); + if (pos == std::string::npos) + continue; + + std::string key = line.substr(0, pos); + std::string value = line.substr(pos + 1); + + trim(key); + trim(value); + + if (!key.empty()) + mTranslations[key] = value; + } + + LOG(LogInfo) << "LocaleES: loaded " << mTranslations.size() + << " entries from " << filePath; + return true; +} + +// -------------------- +// Cargar según Settings +// -------------------- +void LocaleES::loadFromSettings() +{ + std::string lang = Settings::getInstance()->getString("Language"); + if (lang.empty()) + lang = "en"; + + // Si ya está cargado este idioma y hay traducciones, no recargar + if (lang == mCurrentLanguage && !mTranslations.empty()) + return; + + mCurrentLanguage = lang; + mTranslations.clear(); + + // Ruta en el home del usuario: ~/.emulationstation/lang/.ini + std::string home = Utils::FileSystem::getHomePath(); + std::string userPath = home + "/.emulationstation/lang/" + lang + ".ini"; + + // Ruta opcional junto al ejecutable: /lang/.ini + std::string exePath = Utils::FileSystem::getExePath(); + std::string appPath = exePath + "/lang/" + lang + ".ini"; + + if (!loadLanguageFile(userPath)) + { + if (!loadLanguageFile(appPath)) + { + LOG(LogWarning) << "LocaleES: no language file found for '" << lang << "'"; + } + } +} + +// -------------------- +// Traducciones +// -------------------- +std::string LocaleES::translate(const std::string& key) const +{ + auto it = mTranslations.find(key); + if (it != mTranslations.end()) + return it->second; + + // Si no hay traducción, devolvemos la clave tal cual + return key; +} + +std::string LocaleES::get(const std::string& key) +{ + return getInstance().translate(key); +} + +// Función global para usar desde es-core +std::string es_translate(const std::string& key) +{ + return LocaleES::get(key); +} diff --git a/es-app/src/LocaleES.h b/es-app/src/LocaleES.h new file mode 100644 index 0000000000..2e8cadb040 --- /dev/null +++ b/es-app/src/LocaleES.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +class LocaleES +{ +public: + // Singleton + static LocaleES& getInstance(); + + // Carga archivo .ini desde una ruta + bool loadLanguageFile(const std::string& filePath); + + // Carga el idioma según Settings ("Language") + void loadFromSettings(); + + // Traduce una clave usando la instancia global + static std::string get(const std::string& key); + + // Traduce una clave usando esta instancia + std::string translate(const std::string& key) const; + +private: + LocaleES(); // constructor privado (singleton) + + // Evitar copias + LocaleES(const LocaleES&) = delete; + LocaleES& operator=(const LocaleES&) = delete; + + std::string mCurrentLanguage; + std::map mTranslations; +}; + +// Función accesible desde cualquier parte (incluido es-core) +std::string es_translate(const std::string& key); diff --git a/es-app/src/LocaleESHook.cpp b/es-app/src/LocaleESHook.cpp new file mode 100644 index 0000000000..8bea23b202 --- /dev/null +++ b/es-app/src/LocaleESHook.cpp @@ -0,0 +1,9 @@ +#include "LocaleES.h" + +// Ruta relativa: ajusta si tu estructura es distinta +#include "../es-core/src/LocaleESHook.h" + +std::string es_translate(const std::string& key) +{ + return LocaleES::getInstance().translate(key); +} diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index d163d85f0c..d85ea940f3 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -6,25 +6,25 @@ #include MetaDataDecl gameDecls[] = { - // key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd - {"name", MD_STRING, "", false, "name", "enter game name"}, - {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, - {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, - {"image", MD_PATH, "", false, "image", "enter path to image"}, - {"video", MD_PATH , "", false, "video", "enter path to video"}, - {"marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, - {"thumbnail", MD_PATH, "", false, "thumbnail", "enter path to thumbnail"}, - {"rating", MD_RATING, "0", 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"}, - {"favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on"}, - {"hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on" }, - {"kidgame", MD_BOOL, "false", false, "kidgame", "enter kidgame off/on" }, - {"playcount", MD_INT, "0", true, "play count", "enter number of times played"}, - {"lastplayed", MD_TIME, "0", true, "last played", "enter last played date"} + // key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd + {"name", MD_STRING, "", false, "NAME", "ENTER GAME NAME"}, + {"sortname", MD_STRING, "", false, "SORTNAME", "ENTER GAME SORT NAME"}, + {"desc", MD_MULTILINE_STRING, "", false, "DESCRIPTION", "ENTER DESCRIPTION"}, + {"image", MD_PATH, "", false, "IMAGE", "ENTER PATH TO IMAGE"}, + {"video", MD_PATH, "", false, "VIDEO", "ENTER PATH TO VIDEO"}, + {"marquee", MD_PATH, "", false, "MARQUEE", "ENTER PATH TO MARQUEE"}, + {"thumbnail", MD_PATH, "", false, "THUMBNAIL", "ENTER PATH TO THUMBNAIL"}, + {"rating", MD_RATING, "0", 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"}, + {"favorite", MD_BOOL, "false", false, "FAVORITE", "ENTER FAVORITE OFF/ON"}, + {"hidden", MD_BOOL, "false", false, "HIDDEN", "ENTER HIDDEN OFF/ON"}, + {"kidgame", MD_BOOL, "false", false, "KIDGAME", "ENTER KIDGAME OFF/ON"}, + {"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 gameMDD(gameDecls, gameDecls + sizeof(gameDecls) / sizeof(gameDecls[0])); @@ -35,19 +35,19 @@ const inline std::string blankDate() { } MetaDataDecl folderDecls[] = { - {"name", MD_STRING, "", false, "name", "enter game name"}, - {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, - {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, - {"image", MD_PATH, "", false, "image", "enter path to image"}, - {"thumbnail", MD_PATH, "", false, "thumbnail", "enter path to thumbnail"}, - {"video", MD_PATH, "", false, "video", "enter path to video"}, - {"marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, - {"rating", MD_RATING, "0", false, "rating", "enter rating"}, - {"releasedate", MD_DATE, blankDate(), true, "release date", "enter release date"}, - {"developer", MD_STRING, "", false, "developer", "enter game developer"}, - {"publisher", MD_STRING, "", false, "publisher", "enter game publisher"}, - {"genre", MD_STRING, "", false, "genre", "enter game genre"}, - {"players", MD_INT, "", false, "players", "enter number of players"} + {"name", MD_STRING, "", false, "NAME", "ENTER GAME NAME"}, + {"sortname", MD_STRING, "", false, "SORTNAME", "ENTER GAME SORT NAME"}, + {"desc", MD_MULTILINE_STRING, "", false, "DESCRIPTION", "ENTER DESCRIPTION"}, + {"image", MD_PATH, "", false, "IMAGE", "ENTER PATH TO IMAGE"}, + {"thumbnail", MD_PATH, "", false, "THUMBNAIL", "ENTER PATH TO THUMBNAIL"}, + {"video", MD_PATH, "", false, "VIDEO", "ENTER PATH TO VIDEO"}, + {"marquee", MD_PATH, "", false, "MARQUEE", "ENTER PATH TO MARQUEE"}, + {"rating", MD_RATING, "0", false, "RATING", "ENTER RATING"}, + {"releasedate", MD_DATE, blankDate(), true, "RELEASE DATE", "ENTER RELEASE DATE"}, + {"developer", MD_STRING, "", false, "DEVELOPER", "ENTER GAME DEVELOPER"}, + {"publisher", MD_STRING, "", false, "PUBLISHER", "ENTER GAME PUBLISHER"}, + {"genre", MD_STRING, "", false, "GENRE", "ENTER GAME GENRE"}, + {"players", MD_INT, "", false, "PLAYERS", "ENTER NUMBER OF PLAYERS"} }; const std::vector folderMDD(folderDecls, folderDecls + sizeof(folderDecls) / sizeof(folderDecls[0])); @@ -134,7 +134,14 @@ void MetaDataList::set(const std::string& key, const std::string& value) const std::string& MetaDataList::get(const std::string& key) const { - return mMap.at(key); + // FIX: avoid std::out_of_range when key does not exist (e.g. "boxart") + auto it = mMap.find(key); + if (it == mMap.end()) + { + static const std::string empty; + return empty; + } + return it->second; } int MetaDataList::getInt(const std::string& key) const diff --git a/es-app/src/NavigationSounds.h b/es-app/src/NavigationSounds.h new file mode 100644 index 0000000000..4ad920f6d5 --- /dev/null +++ b/es-app/src/NavigationSounds.h @@ -0,0 +1,70 @@ +#pragma once +#ifndef ES_APP_NAVIGATION_SOUNDS_H +#define ES_APP_NAVIGATION_SOUNDS_H + +#include +#include +#include + +#include "ThemeData.h" +#include "Sound.h" + +// Helper simple para obtener sonidos de navegación por nombre lógico. +// Ejemplos de logicalName: "scroll", "back", "select", "favorite", "launch", +// "systembrowse", "quicksysselect". +class NavigationSounds +{ +public: + static std::shared_ptr getFromTheme( + const std::shared_ptr& theme, + const std::string& logicalName) + { + if (!theme) + return nullptr; + + // 1) Nombres candidatos: primero el nombre lógico + std::vector elementNames; + elementNames.push_back(logicalName); + + // 2) Alias de compatibilidad (para temas existentes) + if (logicalName == "scroll") + { + // nombres usados en algunos temas + elementNames.push_back("scrollSound"); + elementNames.push_back("scrollSoundList"); + elementNames.push_back("scrollSoundDetailed"); + } + else if (logicalName == "select") + { + elementNames.push_back("selectSound"); + } + else if (logicalName == "back") + { + elementNames.push_back("backSound"); + } + + // 3) Vistas donde buscamos primero + // "all" → para cuando hagamos + static const char* viewsToCheck[] = { "all", "system", "gamelist", "menu" }; + + for (const char* viewName : viewsToCheck) + { + for (const auto& elemName : elementNames) + { + const ThemeData::ThemeElement* elem = + theme->getElement(viewName, elemName, "sound"); + + if (elem && elem->has("path")) + { + std::string path = elem->get("path"); + if (!path.empty()) + return Sound::get(path); + } + } + } + + return nullptr; + } +}; + +#endif // ES_APP_NAVIGATION_SOUNDS_H diff --git a/es-app/src/Paths.h b/es-app/src/Paths.h new file mode 100644 index 0000000000..afb56a7218 --- /dev/null +++ b/es-app/src/Paths.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "utils/FileSystemUtil.h" + +namespace Paths +{ + // ~/.emulationstation + inline std::string getUserEmulationStationPath() + { + return Utils::FileSystem::getHomePath() + "/.emulationstation"; + } + + // ~/.emulationstation/music + inline std::string getUserMusicPath() + { + return getUserEmulationStationPath() + "/music"; + } + + // ~/RetroPie/music + inline std::string getMusicPath() + { + return Utils::FileSystem::getHomePath() + "/RetroPie/music"; + } +} diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 49f58e1c8f..3bc2e6ab7e 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -22,6 +22,34 @@ std::vector SystemData::sSystemVector; std::vector SystemData::sSystemVectorShuffled; std::ranlux48 SystemData::sURNG = std::ranlux48(std::random_device()()); +namespace +{ + // Trunca el nombre a maxLen caracteres, agregando "..." si hace falta + std::string truncateMostPlayedName(const std::string& name, size_t maxLen) + { + if (name.size() <= maxLen) + return name; + + if (maxLen <= 3) + return name.substr(0, maxLen); + + return name.substr(0, maxLen - 3) + "..."; + } + + static std::string expandHomeTilde(const std::string& p) + { + if (p.empty()) return p; + if (p[0] != '~') return p; + if (p.size() == 1) return Utils::FileSystem::getHomePath(); + if (p[1] == '/') return Utils::FileSystem::getHomePath() + p.substr(1); + return p; + } + + static bool fileExistsAny(const std::string& p) + { + return (!p.empty() && Utils::FileSystem::exists(p)); + } +} SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) @@ -172,7 +200,6 @@ std::vector readList(const std::string& str, const char* delims = " return ret; } - SystemData* SystemData::loadSystem(pugi::xml_node system) { std::string name, fullname, path, cmd, themeFolder, defaultCore; @@ -357,7 +384,7 @@ bool SystemData::loadConfig(Window* window) pThreadPool->wait([window, &processedSystem, systemCount, &systemsNames] { int px = processedSystem - 1; - if (px >= 0 && px < systemsNames.size()) + if (px >= 0 && px < (int)systemsNames.size()) window->renderLoadingScreen(systemsNames.at(px), (float)px / (float)(systemCount + 1)); }, 10); } @@ -375,14 +402,14 @@ bool SystemData::loadConfig(Window* window) delete pThreadPool; if (window != NULL) - window->renderLoadingScreen("Favorites", systemCount == 0 ? 0 : currentSystem / systemCount); + window->renderLoadingScreen("Favorites", systemCount == 0 ? 0 : (float)currentSystem / (float)systemCount); CollectionSystemManager::get()->updateSystemsList(); } else { if (window != NULL) - window->renderLoadingScreen("Favorites", systemCount == 0 ? 0 : currentSystem / systemCount); + window->renderLoadingScreen("Favorites", systemCount == 0 ? 0 : (float)currentSystem / (float)systemCount); CollectionSystemManager::get()->loadCollectionSystems(); } @@ -410,7 +437,7 @@ void SystemData::writeExampleConfig(const std::string& path) " \n" " ~/roms/nes\n" "\n" - " \n" " .nes .NES\n" "\n" @@ -470,7 +497,6 @@ SystemData* SystemData::getNext() const if (it == sSystemVector.cend()) it = sSystemVector.cbegin(); } while (!(*it)->isVisible()); - // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. return *it; } @@ -484,7 +510,6 @@ SystemData* SystemData::getPrev() const if (it == sSystemVector.crend()) it = sSystemVector.crbegin(); } while (!(*it)->isVisible()); - // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. return *it; } @@ -498,7 +523,7 @@ std::string SystemData::getGamelistPath(bool forWrite) const return filePath; filePath = Utils::FileSystem::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 + if(forWrite) Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath)); if(forWrite || Utils::FileSystem::exists(filePath)) return filePath; @@ -508,25 +533,16 @@ std::string SystemData::getGamelistPath(bool forWrite) const std::string SystemData::getThemePath() const { - // where we check for themes, in order: - // 1. [SYSTEM_PATH]/theme.xml - // 2. system theme from currently selected theme set [CURRENT_THEME_PATH]/[SYSTEM]/theme.xml - // 3. default system theme from currently selected theme set [CURRENT_THEME_PATH]/theme.xml - - // first, check game folder std::string localThemePath = mRootFolder->getPath() + "/theme.xml"; if(Utils::FileSystem::exists(localThemePath)) return localThemePath; - // not in game folder, try system theme in theme sets localThemePath = ThemeData::getThemeFromCurrentSet(mThemeFolder); if (Utils::FileSystem::exists(localThemePath)) return localThemePath; - // not system theme, try default system theme in theme set localThemePath = Utils::FileSystem::getParent(Utils::FileSystem::getParent(localThemePath)) + "/theme.xml"; - return localThemePath; } @@ -546,9 +562,10 @@ SystemData* SystemData::getRandomSystem() if (sSystemVectorShuffled.empty()) { - std::copy_if(sSystemVector.begin(), sSystemVector.end(), std::back_inserter(sSystemVectorShuffled), [](SystemData *sd){ return sd->isGameSystem(); }); - if (sSystemVectorShuffled.empty()) return NULL; + std::copy_if(sSystemVector.begin(), sSystemVector.end(), std::back_inserter(sSystemVectorShuffled), + [](SystemData *sd){ return sd->isGameSystem(); }); + if (sSystemVectorShuffled.empty()) return NULL; std::shuffle(sSystemVectorShuffled.begin(), sSystemVectorShuffled.end(), sURNG); } @@ -580,6 +597,179 @@ unsigned int SystemData::getDisplayedGameCount() const return (unsigned int)mRootFolder->getFilesRecursive(GAME, true).size(); } +unsigned int SystemData::getFavoriteCount() const +{ + unsigned int fav = 0; + + auto games = mRootFolder->getFilesRecursive(GAME, true); + for (auto game : games) + { + const std::string& favStr = game->metadata.get("favorite"); + if (favStr == "true" || favStr == "1") + ++fav; + } + + return fav; +} + +unsigned int SystemData::getMostPlayedCount() const +{ + unsigned int maxcount = 0; + + auto games = mRootFolder->getFilesRecursive(GAME, true); + for (auto game : games) + { + int pc = game->metadata.getInt("playcount"); + if (pc > (int)maxcount) + maxcount = (unsigned int)pc; + } + + return maxcount; +} + +std::string SystemData::getMostPlayedName() const +{ + std::string bestName; + int maxcount = -1; + + auto games = mRootFolder->getFilesRecursive(GAME, true); + for (auto game : games) + { + int pc = game->metadata.getInt("playcount"); + if (pc > maxcount) + { + maxcount = pc; + + bestName = game->metadata.get("name"); + if (bestName.empty()) + bestName = Utils::FileSystem::getStem(game->getPath()); + } + } + + // Sin juegos o nadie jugado → mostramos "N/A" + if (maxcount <= 0 || bestName.empty()) + return "N/A"; + + // Límite de 22 caracteres como acordamos + return truncateMostPlayedName(bestName, 22); +} + +std::string SystemData::getMostPlayedFull() const +{ + std::string name = getMostPlayedName(); + + if (name == "N/A") + return "N/A"; + + unsigned int count = getMostPlayedCount(); + if (count == 0) + return name; + + return name + " (" + std::to_string(count) + ")"; +} + +// ✅ NUEVO: imagen del más jugado con fallback pulido (boxart → image → thumbnail → titleshot → marquee) +std::string SystemData::getMostPlayedImage() const +{ + FileData* best = nullptr; + int maxcount = -1; + + auto games = mRootFolder->getFilesRecursive(GAME, true); + for (auto game : games) + { + int pc = game->metadata.getInt("playcount"); + if (pc > maxcount) + { + maxcount = pc; + best = game; + } + } + + if (!best || maxcount <= 0) + return ""; + + // Orden pulido (lo que acordamos): boxart → image → thumbnail → (titleshot opcional) → marquee + std::string raw = best->metadata.get("boxart"); + if (raw.empty()) raw = best->metadata.get("image"); + if (raw.empty()) raw = best->metadata.get("thumbnail"); + if (raw.empty()) raw = best->metadata.get("titleshot"); // opcional, lo dejamos porque ya lo usabas + if (raw.empty()) raw = best->metadata.get("marquee"); + if (raw.empty()) + return ""; + + raw = expandHomeTilde(raw); + + // 1) absoluta y existe + if (!raw.empty() && raw[0] == '/' && fileExistsAny(raw)) + return raw; + + // 2) relativa -> resolver contra carpeta del gamelist + { + std::string base = Utils::FileSystem::getParent(getGamelistPath(false)); + std::string candidate = raw; + + if (candidate.size() >= 2 && candidate[0] == '.' && candidate[1] == '/') + candidate = candidate.substr(2); + + candidate = Utils::FileSystem::getGenericPath(base + "/" + candidate); + if (fileExistsAny(candidate)) + return candidate; + } + + // 3) fallback por filename a tus 2 rutas + std::string filename = Utils::FileSystem::getFileName(raw); + if (filename.empty()) + return ""; + + // A) ~/.emulationstation/downloaded_images// + { + std::string p = Utils::FileSystem::getHomePath() + + "/.emulationstation/downloaded_images/" + + mName + "/" + filename; + + p = Utils::FileSystem::getGenericPath(p); + if (fileExistsAny(p)) + return p; + } + + // B) ~/RetroPie/roms//media/screenshots/ + { + std::string p = Utils::FileSystem::getHomePath() + + "/RetroPie/roms/" + + mName + "/media/screenshots/" + filename; + + p = Utils::FileSystem::getGenericPath(p); + if (fileExistsAny(p)) + return p; + } + + // 4) si no hay extensión, probar comunes + std::string ext = Utils::FileSystem::getExtension(filename); + std::string stem = Utils::FileSystem::getStem(filename); + + if (ext.empty() && !stem.empty()) + { + const char* exts[] = { ".png", ".jpg", ".jpeg", ".webp" }; + + for (auto e : exts) + { + std::string pA = Utils::FileSystem::getHomePath() + + "/.emulationstation/downloaded_images/" + mName + "/" + stem + e; + pA = Utils::FileSystem::getGenericPath(pA); + if (fileExistsAny(pA)) + return pA; + + std::string pB = Utils::FileSystem::getHomePath() + + "/RetroPie/roms/" + mName + "/media/screenshots/" + stem + e; + pB = Utils::FileSystem::getGenericPath(pB); + if (fileExistsAny(pB)) + return pB; + } + } + + return ""; +} + void SystemData::loadTheme() { mTheme = std::make_shared(); @@ -593,12 +783,24 @@ void SystemData::loadTheme() { // build map with system variables for theme to use, std::map sysData; - sysData.insert(std::pair("system.name", getName())); - sysData.insert(std::pair("system.theme", getThemeFolder())); - sysData.insert(std::pair("system.fullName", getFullName())); + sysData.insert(std::make_pair("system.name", getName())); + sysData.insert(std::make_pair("system.theme", getThemeFolder())); + sysData.insert(std::make_pair("system.fullName", getFullName())); + + // Extras para temas (contadores y "más jugado") + sysData.insert(std::make_pair("system.gameCount", std::to_string(getGameCount()))); + sysData.insert(std::make_pair("system.displayedGameCount", std::to_string(getDisplayedGameCount()))); + sysData.insert(std::make_pair("system.favoriteCount", std::to_string(getFavoriteCount()))); + sysData.insert(std::make_pair("system.mostPlayedCount", std::to_string(getMostPlayedCount()))); + sysData.insert(std::make_pair("system.mostPlayedName", getMostPlayedName())); + sysData.insert(std::make_pair("system.mostPlayedFull", getMostPlayedFull())); + + // ✅ NUEVO: ruta de imagen resuelta + sysData.insert(std::make_pair("system.mostPlayedImage", getMostPlayedImage())); mTheme->loadFile(sysData, path); - } catch(ThemeException& e) + } + catch(ThemeException& e) { LOG(LogError) << e.what(); mTheme = std::make_shared(); // reset to empty diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index d440239130..4276c40284 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -27,7 +27,11 @@ struct SystemEnvironmentData class SystemData { public: - SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem = false); + SystemData(const std::string& name, + const std::string& fullName, + SystemEnvironmentData* envData, + const std::string& themeFolder, + bool CollectionSystem = false); ~SystemData(); inline FileData* getRootFolder() const { return mRootFolder; }; @@ -38,7 +42,13 @@ class SystemData inline const std::string& getThemeFolder() const { return mThemeFolder; } inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } inline const std::vector& getPlatformIds() const { return mEnvData->mPlatformIds; } - inline bool hasPlatformId(PlatformIds::PlatformId id) { if (!mEnvData) return false; return std::find(mEnvData->mPlatformIds.cbegin(), mEnvData->mPlatformIds.cend(), id) != mEnvData->mPlatformIds.cend(); } + inline bool hasPlatformId(PlatformIds::PlatformId id) + { + if (!mEnvData) return false; + return std::find(mEnvData->mPlatformIds.cbegin(), + mEnvData->mPlatformIds.cend(), + id) != mEnvData->mPlatformIds.cend(); + } inline const std::shared_ptr& getTheme() const { return mTheme; } @@ -49,17 +59,32 @@ class SystemData unsigned int getGameCount() const; unsigned int getDisplayedGameCount() const; + // NUEVO: contadores para favoritos y más jugados + unsigned int getFavoriteCount() const; + unsigned int getMostPlayedCount() const; + std::string getMostPlayedName() const; + std::string getMostPlayedFull() const; + + // NUEVO: imagen del juego más jugado (para usar en el tema) + std::string getMostPlayedImage() const; + static void deleteSystems(); - static bool loadConfig(Window* window); //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 bool loadConfig(Window* window); 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::string getConfigPath(bool forWrite); static std::vector sSystemVector; static std::vector sSystemVectorShuffled; static std::ranlux48 sURNG; - inline std::vector::const_iterator getIterator() const { return std::find(sSystemVector.cbegin(), sSystemVector.cend(), this); }; - inline std::vector::const_reverse_iterator getRevIterator() const { return std::find(sSystemVector.crbegin(), sSystemVector.crend(), this); }; + inline std::vector::const_iterator getIterator() const + { + return std::find(sSystemVector.cbegin(), sSystemVector.cend(), this); + }; + inline std::vector::const_reverse_iterator getRevIterator() const + { + return std::find(sSystemVector.crbegin(), sSystemVector.crend(), this); + }; inline bool isCollection() { return mIsCollectionSystem; }; inline bool isGameSystem() { return mIsGameSystem; }; @@ -71,7 +96,6 @@ class SystemData static SystemData* getRandomSystem(); FileData* getRandomGame(); - // Load or re-load theme. void loadTheme(); FileFilterIndex* getIndex() { return mFilterIndex; }; @@ -97,7 +121,6 @@ class SystemData FileFilterIndex* mFilterIndex; FileData* mRootFolder; - // for getRandomGame() std::vector mGamesShuffled; }; diff --git a/es-app/src/ThemeOptions.cpp b/es-app/src/ThemeOptions.cpp new file mode 100644 index 0000000000..a85a877afd --- /dev/null +++ b/es-app/src/ThemeOptions.cpp @@ -0,0 +1,5 @@ +// ThemeOptions.cpp +// Archivo de compatibilidad vacío para ES-X. +// La lógica real de opciones de tema está en guis/GuiThemeOptions.* +// Este archivo se mantiene sólo porque está referenciado en CMakeLists. + diff --git a/es-app/src/ThemeOptions.h b/es-app/src/ThemeOptions.h new file mode 100644 index 0000000000..d6520d6e53 --- /dev/null +++ b/es-app/src/ThemeOptions.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +// Gestor simple de opciones por tema, basado en un INI: +// [Pi-Station-X] +// LAYOUT=ps4 +// AVATAR=avatar03 +// START_LABEL=INICIO +class ThemeOptions +{ +public: + // Singleton + static ThemeOptions& getInstance(); + + // Carga desde un INI. Si path == "" usa: + // ~/.emulationstation/theme-options.ini + void load(const std::string& path = ""); + + // Guarda el INI en el mismo path usado en load() + // o en el path por defecto si no se cargó ninguno. + void save(); + + // Lee una opción: sección = nombre del theme. + // Si no existe, devuelve def. + std::string get(const std::string& themeName, + const std::string& key, + const std::string& def = "") const; + + // Escribe/actualiza una opción en memoria. + // NO guarda en disco hasta que se llame a save(). + void set(const std::string& themeName, + const std::string& key, + const std::string& value); + + const std::string& getPath() const { return mPath; } + +private: + ThemeOptions(); + ThemeOptions(const ThemeOptions&) = delete; + ThemeOptions& operator=(const ThemeOptions&) = delete; + + void clear(); + void parseLine(const std::string& line, std::string& currentSection); + + std::string mPath; + // mData[section][key] = value + std::map> mData; +}; diff --git a/es-app/src/audio/BackgroundMusicManager.cpp b/es-app/src/audio/BackgroundMusicManager.cpp new file mode 100644 index 0000000000..0b09e54c87 --- /dev/null +++ b/es-app/src/audio/BackgroundMusicManager.cpp @@ -0,0 +1,497 @@ +#include "audio/BackgroundMusicManager.h" + +#include "Settings.h" +#include "Log.h" +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" + +#include +#include + +#include +#include +#include + +// ============================= +// Singleton +// ============================= + +BackgroundMusicManager* BackgroundMusicManager::sInstance = nullptr; + +BackgroundMusicManager& BackgroundMusicManager::getInstance() +{ + if (!sInstance) + sInstance = new BackgroundMusicManager(); + return *sInstance; +} + +// ============================= +// Ctor / Dtor +// ============================= + +BackgroundMusicManager::BackgroundMusicManager() + : mInitialized(false) + , mEnabled(true) + , mGameRunning(false) + , mMixerOpenedByUs(false) + , mCurrentIndex(-1) + , mCurrentMusic(nullptr) + , mNowPlayingText("") + , mSongNameChanged(false) + , mPendingResume(false) + , mResumeDelayMs(600) + , mResumeTimerMs(0) + , mPendingIndex(-1) + , mPendingReopenOnResume(false) + , mPendingNextFromCallback(false) +{ + mEnabled = Settings::getInstance()->getBool("BackgroundMusic"); +} + +BackgroundMusicManager::~BackgroundMusicManager() +{ + shutdown(); +} + +// ============================= +// Popup helpers +// ============================= + +bool BackgroundMusicManager::songNameChanged() +{ + return mSongNameChanged; +} + +void BackgroundMusicManager::resetSongNameChangedFlag() +{ + mSongNameChanged = false; +} + +std::string BackgroundMusicManager::getCurrentSongDisplayName() const +{ + return mNowPlayingText; +} + +void BackgroundMusicManager::setNowPlaying(const std::string& fullPath) +{ + std::string stem = Utils::FileSystem::getStem(fullPath); + if (stem.empty()) + stem = fullPath; + + mNowPlayingText = std::string("🎵 Now playing: ") + stem; + mSongNameChanged = true; +} + +// ============================= +// Mixer open/close/reopen +// ============================= + +bool BackgroundMusicManager::openMixer() +{ + if (mMixerOpenedByUs) + return true; + + if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) + { + LOG(LogError) << "BGM - Mix_OpenAudio failed: " << Mix_GetError(); + return false; + } + + Mix_AllocateChannels(16); + Mix_HaltMusic(); + Mix_HookMusicFinished(&BackgroundMusicManager::musicFinishedCallbackStatic); + + mMixerOpenedByUs = true; + LOG(LogInfo) << "BGM - Mixer OPEN"; + return true; +} + +void BackgroundMusicManager::closeMixer() +{ + if (!mMixerOpenedByUs) + return; + + mPendingResume = false; + mResumeTimerMs = 0; + mPendingIndex = -1; + mPendingReopenOnResume = false; + mPendingNextFromCallback.store(false); + + stopMusicInternal(false); + Mix_HookMusicFinished(nullptr); + Mix_HaltMusic(); + Mix_CloseAudio(); + + mMixerOpenedByUs = false; + LOG(LogInfo) << "BGM - Mixer CLOSED"; +} + +bool BackgroundMusicManager::reopenMixer() +{ + if (mMixerOpenedByUs) + { + stopMusicInternal(false); + Mix_HookMusicFinished(nullptr); + Mix_HaltMusic(); + Mix_CloseAudio(); + mMixerOpenedByUs = false; + LOG(LogInfo) << "BGM - Mixer REOPEN (close)"; + } + + return openMixer(); +} + +// ============================= +// Init / Shutdown +// ============================= + +void BackgroundMusicManager::init() +{ + if (mInitialized) + return; + + if (!openMixer()) + return; + + buildPlaylist(); + mInitialized = true; + + LOG(LogInfo) << "BGM - Initialized | playlist=" << mPlaylist.size() + << " | enabled=" << (mEnabled ? "1" : "0"); + + if (mEnabled && !mPlaylist.empty()) + playCurrent(); +} + +void BackgroundMusicManager::shutdown() +{ + closeMixer(); + + mInitialized = false; + mPlaylist.clear(); + mLastPlayed.clear(); + mCurrentIndex = -1; + + LOG(LogInfo) << "BGM - Shutdown"; +} + +// ============================= +// Enable / Disable +// ============================= + +void BackgroundMusicManager::setEnabled(bool enabled) +{ + if (enabled == mEnabled) + return; + + mEnabled = enabled; + Settings::getInstance()->setBool("BackgroundMusic", enabled); + + LOG(LogInfo) << "BGM - setEnabled(" << (enabled ? "true" : "false") << ")"; + + if (!mEnabled) + { + mPendingResume = false; + mResumeTimerMs = 0; + mPendingIndex = -1; + mPendingReopenOnResume = false; + mPendingNextFromCallback.store(false); + + stopMusicInternal(true); + return; + } + + if (!mInitialized) + { + init(); + return; + } + + if (!openMixer()) + return; + + if (mPlaylist.empty()) + buildPlaylist(); + + if (!mPlaylist.empty()) + playCurrent(); +} + +// ============================= +// Game hooks +// ============================= + +void BackgroundMusicManager::onGameLaunched() +{ + if (!mInitialized) + return; + + mGameRunning = true; + + mPendingResume = false; + mResumeTimerMs = 0; + mPendingIndex = -1; + mPendingNextFromCallback.store(false); + + LOG(LogInfo) << "BGM - onGameLaunched() -> stop music"; + stopMusicInternal(true); +} + +void BackgroundMusicManager::onGameEnded() +{ + mGameRunning = false; + + if (!mEnabled || !mInitialized) + return; + + if (!mPlaylist.empty()) + mPendingIndex = pickNextIndex(); + else + mPendingIndex = -1; + + mPendingReopenOnResume = true; + mPendingResume = true; + mResumeTimerMs = mResumeDelayMs; + + LOG(LogInfo) << "BGM - onGameEnded(): resume in " << mResumeDelayMs << "ms"; +} + +// ============================= +// Update (main thread) +// ============================= + +void BackgroundMusicManager::update(int deltaTimeMs) +{ + if (!mInitialized) + return; + + if (mPendingNextFromCallback.exchange(false)) + { + if (!mGameRunning && mEnabled) + playNext(); + } + + if (!mPendingResume) + return; + + if (mGameRunning || !mEnabled) + { + mPendingResume = false; + mResumeTimerMs = 0; + mPendingIndex = -1; + mPendingReopenOnResume = false; + return; + } + + mResumeTimerMs -= deltaTimeMs; + if (mResumeTimerMs > 0) + return; + + mPendingResume = false; + mResumeTimerMs = 0; + + if (mPlaylist.empty()) + buildPlaylist(); + + if (mPlaylist.empty()) + return; + + if (mPendingReopenOnResume) + { + mPendingReopenOnResume = false; + if (!reopenMixer()) + return; + } + else + { + if (!openMixer()) + return; + } + + if (mPendingIndex >= 0 && mPendingIndex < (int)mPlaylist.size()) + mCurrentIndex = mPendingIndex; + else + mCurrentIndex = pickNextIndex(); + + mPendingIndex = -1; + + LOG(LogInfo) << "BGM - resume -> playCurrent() index=" << mCurrentIndex; + playCurrent(); +} + +// ============================= +// Playlist +// ============================= + +void BackgroundMusicManager::buildPlaylist() +{ + mPlaylist.clear(); + mLastPlayed.clear(); + mCurrentIndex = -1; + + const std::string home = Utils::FileSystem::getHomePath(); + buildPlaylistFromPath(home + "/RetroPie/music"); + buildPlaylistFromPath(home + "/.emulationstation/music"); + + if (!mPlaylist.empty()) + { + static std::mt19937 rng{ std::random_device{}() }; + std::uniform_int_distribution dist(0, (int)mPlaylist.size() - 1); + mCurrentIndex = dist(rng); + + LOG(LogInfo) << "BGM - Playlist ready (" << mPlaylist.size() << " tracks)"; + } +} + +void BackgroundMusicManager::buildPlaylistFromPath(const std::string& path) +{ + if (!Utils::FileSystem::exists(path) || !Utils::FileSystem::isDirectory(path)) + return; + + for (const auto& e : Utils::FileSystem::getDirContent(path)) + if (!Utils::FileSystem::isDirectory(e) && isValidAudioFile(e)) + mPlaylist.push_back(e); +} + +bool BackgroundMusicManager::isValidAudioFile(const std::string& path) const +{ + const std::string ext = Utils::String::toLower(Utils::FileSystem::getExtension(path)); + return (ext == ".mp3" || ext == ".ogg" || ext == ".flac" || ext == ".wav" || ext == ".mod"); +} + +// ============================= +// Shuffle inteligente +// ============================= + +void BackgroundMusicManager::addLastPlayed(const std::string& song) +{ + int maxHistory = std::max(1, (int)std::floor(mPlaylist.size() * 0.4f)); + + mLastPlayed.push_front(song); + while ((int)mLastPlayed.size() > maxHistory) + mLastPlayed.pop_back(); +} + +bool BackgroundMusicManager::wasPlayedRecently(const std::string& song) const +{ + return std::find(mLastPlayed.begin(), mLastPlayed.end(), song) != mLastPlayed.end(); +} + +int BackgroundMusicManager::pickNextIndex() +{ + if (mPlaylist.size() <= 1) + return mCurrentIndex; + + static std::mt19937 rng{ std::random_device{}() }; + std::uniform_int_distribution dist(0, (int)mPlaylist.size() - 1); + + int tries = 0; + int idx = mCurrentIndex; + + do { + idx = dist(rng); + tries++; + } while (tries < 20 && wasPlayedRecently(mPlaylist[idx])); + + return idx; +} + +// ============================= +// Playback +// ============================= + +void BackgroundMusicManager::playCurrent() +{ + if (!mInitialized || !mEnabled || !mMixerOpenedByUs || mPlaylist.empty()) + return; + + if (Mix_PlayingMusic() && !Mix_PausedMusic()) + return; + + int attempts = 0; + int maxAttempts = std::min((int)mPlaylist.size(), 10); + + while (attempts < maxAttempts && !mPlaylist.empty()) + { + if (mCurrentIndex < 0 || mCurrentIndex >= (int)mPlaylist.size()) + mCurrentIndex = pickNextIndex(); + + const std::string song = mPlaylist[mCurrentIndex]; + + if (mCurrentMusic) + { + Mix_HaltMusic(); + Mix_FreeMusic(mCurrentMusic); + mCurrentMusic = nullptr; + } + + mCurrentMusic = Mix_LoadMUS(song.c_str()); + if (!mCurrentMusic) + { + LOG(LogError) << "BGM - Load failed, skipping: " << song + << " (" << Mix_GetError() << ")"; + + mPlaylist.erase(mPlaylist.begin() + mCurrentIndex); + mCurrentIndex = -1; + attempts++; + continue; + } + + LOG(LogInfo) << "BGM - Playing: " << song; + setNowPlaying(song); + + Mix_PlayMusic(mCurrentMusic, 0); + addLastPlayed(song); + return; + } + + LOG(LogWarning) << "BGM - No valid tracks left, stopping music"; +} + +// ============================= +// Controls +// ============================= + +void BackgroundMusicManager::playNext() +{ + if (!mInitialized || mPlaylist.empty()) + return; + + mCurrentIndex = pickNextIndex(); + playCurrent(); +} + +void BackgroundMusicManager::stopMusicInternal(bool fadeOut) +{ + if (!mMixerOpenedByUs) + return; + + if (fadeOut) + Mix_FadeOutMusic(500); + else + Mix_HaltMusic(); + + if (mCurrentMusic) + { + Mix_FreeMusic(mCurrentMusic); + mCurrentMusic = nullptr; + } +} + +// ============================= +// Callback +// ============================= + +void BackgroundMusicManager::musicFinishedCallbackStatic() +{ + if (sInstance) + sInstance->musicFinishedCallback(); +} + +void BackgroundMusicManager::musicFinishedCallback() +{ + if (mGameRunning || !mEnabled) + return; + + mPendingNextFromCallback.store(true); +} diff --git a/es-app/src/audio/BackgroundMusicManager.h b/es-app/src/audio/BackgroundMusicManager.h new file mode 100644 index 0000000000..cc967e3ac7 --- /dev/null +++ b/es-app/src/audio/BackgroundMusicManager.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include +#include + +struct _Mix_Music; +typedef struct _Mix_Music Mix_Music; + +class BackgroundMusicManager +{ +public: + static BackgroundMusicManager& getInstance(); + + void init(); + void shutdown(); + + void setEnabled(bool enabled); + bool isEnabled() const { return mEnabled; } + + void onGameLaunched(); + void onGameEnded(); + + void update(int deltaTimeMs); + + void playNext(); + + bool songNameChanged(); + void resetSongNameChangedFlag(); + std::string getCurrentSongDisplayName() const; + +private: + BackgroundMusicManager(); + ~BackgroundMusicManager(); + + bool openMixer(); + void closeMixer(); + bool reopenMixer(); + + void buildPlaylist(); + void buildPlaylistFromPath(const std::string& path); + bool isValidAudioFile(const std::string& path) const; + + void addLastPlayed(const std::string& song); + bool wasPlayedRecently(const std::string& song) const; + int pickNextIndex(); + + void playCurrent(); + void stopMusicInternal(bool fadeOut); + + void setNowPlaying(const std::string& fullPath); + + static void musicFinishedCallbackStatic(); + void musicFinishedCallback(); + +private: + static BackgroundMusicManager* sInstance; + + bool mInitialized; + bool mEnabled; + bool mGameRunning; + bool mMixerOpenedByUs; + + std::vector mPlaylist; + std::deque mLastPlayed; + int mCurrentIndex; + + Mix_Music* mCurrentMusic; + + std::string mNowPlayingText; + bool mSongNameChanged; + + bool mPendingResume; + int mResumeDelayMs; + int mResumeTimerMs; + int mPendingIndex; + + bool mPendingReopenOnResume; + + // ⚠️ cruzado entre audio thread y main thread + std::atomic mPendingNextFromCallback; +}; diff --git a/es-app/src/components/TextListComponent.h b/es-app/src/components/TextListComponent.h index 2250769d30..d5484a207c 100644 --- a/es-app/src/components/TextListComponent.h +++ b/es-app/src/components/TextListComponent.h @@ -189,7 +189,7 @@ void TextListComponent::render(const Transform4x4f& parentTrans) Renderer::pushClipRect(Vector2i((int)(trans.translation().x() + mHorizontalMargin), (int)trans.translation().y()), Vector2i((int)(dim.x() - mHorizontalMargin*2), (int)dim.y())); - for(int i = mViewportTop; i < listCutoff; i++) + for(int i = mViewportTop; i < (int)listCutoff; i++) { typename IList::Entry& entry = mEntries.at((unsigned int)i); @@ -427,8 +427,27 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, c const float selectorHeight = Math::max(mFont->getHeight(1.0), (float)mFont->getSize()) * mLineSpacing; setSelectorHeight(selectorHeight); - if(properties & SOUND && elem->has("scrollSound")) - mScrollSound = elem->get("scrollSound"); + // 🔊 SONIDO DE SCROLL + if(properties & SOUND) + { + // 1) Prioridad: scrollSound definido directamente en + if(elem->has("scrollSound")) + { + mScrollSound = elem->get("scrollSound"); + } + else + { + // 2) Si no hay en textlist, usar feature + // con ... + const ThemeData::ThemeElement* sndElem = + theme->getElement("all", "scroll", "sound"); + + if (sndElem && sndElem->has("path")) + { + mScrollSound = sndElem->get("path"); + } + } + } if(properties & ALIGNMENT) { diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index 1019e70780..8bf60bbe15 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -4,6 +4,8 @@ #include "components/OptionListComponent.h" #include "components/SwitchComponent.h" +#include "components/TextComponent.h" +#include "components/ImageComponent.h" #include "guis/GuiInfoPopup.h" #include "guis/GuiRandomCollectionOptions.h" #include "guis/GuiSettings.h" @@ -12,9 +14,27 @@ #include "views/ViewController.h" #include "CollectionSystemManager.h" #include "Window.h" +#include "Settings.h" +#include "renderers/Renderer.h" -GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) : GuiComponent(window), mMenu(window, "GAME COLLECTION SETTINGS") +#include "LocaleES.h" + +// Helper local para traducciones .ini +namespace +{ + inline std::string _(const std::string& key) + { + return LocaleES::getInstance().translate(key); + } +} + +GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) + : GuiComponent(window) + , mMenu(window, _("GAME COLLECTION SETTINGS").c_str()) { + // Asegurar que el idioma actual esté cargado + LocaleES::getInstance().loadFromSettings(); + initializeMenu(); } @@ -22,102 +42,156 @@ void GuiCollectionSystemsOptions::initializeMenu() { addChild(&mMenu); - // get collections + // obtener colecciones addSystemsToMenu(); - // manage random collection - addEntry("RANDOM GAME COLLECTION SETTINGS", 0x777777FF, true, [this] { openRandomCollectionSettings(); }); - + // opción de ajustes de colección aleatoria + addEntry(_("RANDOM GAME COLLECTION SETTINGS").c_str(), 0x777777FF, true, + [this] { openRandomCollectionSettings(); }); ComponentListRow row; - if(CollectionSystemManager::get()->isEditing()) + if (CollectionSystemManager::get()->isEditing()) { - row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + std::string label = + _("FINISH EDITING") + std::string(" '") + + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + + std::string("' ") + _("COLLECTION"); + + row.addElement(std::make_shared( + mWindow, + label, + Font::get(FONT_SIZE_MEDIUM), + 0x777777FF), + true); + row.makeAcceptInputHandler(std::bind(&GuiCollectionSystemsOptions::exitEditMode, this)); mMenu.addRow(row); } else { - // add "Create New Custom Collection from Theme" + // Crear nueva colección desde el tema std::vector unusedFolders = CollectionSystemManager::get()->getUnusedSystemsFromTheme(); - if (unusedFolders.size() > 0) + if (!unusedFolders.empty()) { - addEntry("CREATE NEW CUSTOM COLLECTION FROM THEME", 0x777777FF, true, - [this, unusedFolders] { - auto s = new GuiSettings(mWindow, "SELECT THEME FOLDER"); - std::shared_ptr< OptionListComponent > folderThemes = std::make_shared< OptionListComponent >(mWindow, "SELECT THEME FOLDER", true); - - // add Custom Systems - for(auto it = unusedFolders.cbegin() ; it != unusedFolders.cend() ; it++ ) - { - ComponentListRow row; - std::string name = *it; - - std::function createCollectionCall = [name, this, s] { - createCollection(name); - }; - row.makeAcceptInputHandler(createCollectionCall); - - auto themeFolder = std::make_shared(mWindow, Utils::String::toUpper(name), Font::get(FONT_SIZE_SMALL), 0x777777FF); - row.addElement(themeFolder, true); - s->addRow(row); - } - mWindow->pushGui(s); - }); + addEntry(_("CREATE NEW CUSTOM COLLECTION FROM THEME").c_str(), 0x777777FF, true, + [this, unusedFolders] { + auto s = new GuiSettings(mWindow, _("SELECT THEME FOLDER").c_str()); + std::shared_ptr< OptionListComponent > folderThemes = + std::make_shared< OptionListComponent >( + mWindow, + _("SELECT THEME FOLDER"), + true); + + // añadir sistemas del tema + for (auto it = unusedFolders.cbegin(); it != unusedFolders.cend(); ++it) + { + ComponentListRow row; + + std::string name = *it; + std::function createCollectionCall = [name, this, s] { + createCollection(name); + }; + row.makeAcceptInputHandler(createCollectionCall); + + auto themeFolder = std::make_shared( + mWindow, + Utils::String::toUpper(name), + Font::get(FONT_SIZE_SMALL), + 0x777777FF); + + row.addElement(themeFolder, true); + s->addRow(row); + } + + mWindow->pushGui(s); + }); } - row.addElement(std::make_shared(mWindow, "CREATE NEW CUSTOM COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + // Crear nueva colección personalizada + row.elements.clear(); + row.addElement(std::make_shared( + mWindow, + _("CREATE NEW CUSTOM COLLECTION"), + Font::get(FONT_SIZE_MEDIUM), + 0x777777FF), + true); + auto createCustomCollection = [this](const std::string& newVal) { std::string name = newVal; - // we need to store the first Gui and remove it, as it'll be deleted by the actual Gui + // hay que guardar el primer Gui y quitarlo, porque será borrado por el nuevo Gui Window* window = mWindow; GuiComponent* topGui = window->peekGui(); window->removeGui(topGui); createCollection(name); }; + row.makeAcceptInputHandler([this, createCustomCollection] { - mWindow->pushGui(new GuiTextEditPopup(mWindow, "New Collection Name", "", createCustomCollection, false)); - }); + mWindow->pushGui(new GuiTextEditPopup( + mWindow, + _("NEW COLLECTION NAME"), + "", + createCustomCollection, + false)); + }); mMenu.addRow(row); } + // agrupar colecciones sin tema bundleCustomCollections = std::make_shared(mWindow); bundleCustomCollections->setState(Settings::getInstance()->getBool("UseCustomCollectionsSystem")); - mMenu.addWithLabel("GROUP UNTHEMED CUSTOM COLLECTIONS", bundleCustomCollections); + mMenu.addWithLabel(_("GROUP UNTHEMED CUSTOM COLLECTIONS"), bundleCustomCollections); + // ordenar colecciones y sistemas sortAllSystemsSwitch = std::make_shared(mWindow); sortAllSystemsSwitch->setState(Settings::getInstance()->getBool("SortAllSystems")); - mMenu.addWithLabel("SORT CUSTOM COLLECTIONS AND SYSTEMS", sortAllSystemsSwitch); + mMenu.addWithLabel(_("SORT CUSTOM COLLECTIONS AND SYSTEMS"), sortAllSystemsSwitch); + // mostrar nombre del sistema en colecciones toggleSystemNameInCollections = std::make_shared(mWindow); toggleSystemNameInCollections->setState(Settings::getInstance()->getBool("CollectionShowSystemInfo")); - mMenu.addWithLabel("SHOW SYSTEM NAME IN COLLECTIONS", toggleSystemNameInCollections); + mMenu.addWithLabel(_("SHOW SYSTEM NAME IN COLLECTIONS"), toggleSystemNameInCollections); - // double press to remove from favorites + // doble pulsación en Y para quitar de favoritos/colección doublePressToRemoveFavs = std::make_shared(mWindow); doublePressToRemoveFavs->setState(Settings::getInstance()->getBool("DoublePressRemovesFromFavs")); - mMenu.addWithLabel("PRESS (Y) TWICE TO REMOVE FROM FAVS./COLL.", doublePressToRemoveFavs); - - - // Add option to select default collection for screensaver - defaultScreenSaverCollection = std::make_shared< OptionListComponent >(mWindow, "ADD/REMOVE GAMES WHILE SCREENSAVER TO", false); - // Add default option - std::string defaultScreenSaverCollectionName = Settings::getInstance()->getString("DefaultScreenSaverCollection"); - defaultScreenSaverCollection->add("", "", defaultScreenSaverCollectionName == ""); - - std::map customSystems = CollectionSystemManager::get()->getCustomCollectionSystems(); - // add all enabled Custom Systems - for(std::map::const_iterator it = customSystems.cbegin() ; it != customSystems.cend() ; it++ ) + mMenu.addWithLabel(_("PRESS (Y) TWICE TO REMOVE FROM FAVS./COLL."), doublePressToRemoveFavs); + + // opción de colección por defecto para screensaver + defaultScreenSaverCollection = + std::make_shared< OptionListComponent >( + mWindow, + _("ADD/REMOVE GAMES WHILE SCREENSAVER TO"), + false); + + // opción por defecto + std::string defaultScreenSaverCollectionName = + Settings::getInstance()->getString("DefaultScreenSaverCollection"); + defaultScreenSaverCollection->add( + _(""), + "", + defaultScreenSaverCollectionName == ""); + + std::map customSystems = + CollectionSystemManager::get()->getCustomCollectionSystems(); + + // añadir sistemas personalizados habilitados + for (auto it = customSystems.cbegin(); it != customSystems.cend(); ++it) { if (it->second.isEnabled) - defaultScreenSaverCollection->add(it->second.decl.longName, it->second.decl.name, defaultScreenSaverCollectionName == it->second.decl.name); + defaultScreenSaverCollection->add( + it->second.decl.longName, + it->second.decl.name, + defaultScreenSaverCollectionName == it->second.decl.name); } - mMenu.addWithLabel("ADD/REMOVE GAMES WHILE SCREENSAVER TO", defaultScreenSaverCollection); + mMenu.addWithLabel(_("ADD/REMOVE GAMES WHILE SCREENSAVER TO"), defaultScreenSaverCollection); - mMenu.addButton("BACK", "back", std::bind(&GuiCollectionSystemsOptions::applySettings, this)); + // botón volver + mMenu.addButton(_("BACK"), "back", std::bind(&GuiCollectionSystemsOptions::applySettings, this)); - mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); + mMenu.setPosition( + (Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, + Renderer::getScreenHeight() * 0.15f); } void GuiCollectionSystemsOptions::addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func) @@ -128,7 +202,7 @@ void GuiCollectionSystemsOptions::addEntry(const char* name, unsigned int color, ComponentListRow row; row.addElement(std::make_shared(mWindow, name, font, color), true); - if(add_arrow) + if (add_arrow) { std::shared_ptr bracket = makeArrow(mWindow); row.addElement(bracket, false); @@ -145,11 +219,16 @@ void GuiCollectionSystemsOptions::createCollection(std::string inName) std::string name = collSysMgr->getValidNewCollectionName(inName); SystemData* newSys = collSysMgr->addNewCustomCollection(name, true); - if (!collSysMgr->saveCustomCollection(newSys)) { - GuiInfoPopup* s = new GuiInfoPopup(mWindow, "Failed creating '" + Utils::String::toUpper(name) + "' Collection. See log for details.", 8000); + if (!collSysMgr->saveCustomCollection(newSys)) + { + GuiInfoPopup* s = new GuiInfoPopup( + mWindow, + "Failed creating '" + Utils::String::toUpper(name) + "' Collection. See log for details.", + 8000); mWindow->setInfoPopup(s); return; } + customOptionList->add(name, name, true); std::string outAuto = Utils::String::vectorToDelimitedString(autoOptionList->getSelectedObjects(), ","); std::string outCustom = Utils::String::vectorToDelimitedString(customOptionList->getSelectedObjects(), ","); @@ -158,7 +237,7 @@ void GuiCollectionSystemsOptions::createCollection(std::string inName) Window* window = mWindow; collSysMgr->setEditMode(name); - while(window->peekGui() && window->peekGui() != ViewController::get()) + while (window->peekGui() && window->peekGui() != ViewController::get()) delete window->peekGui(); } @@ -175,33 +254,57 @@ void GuiCollectionSystemsOptions::exitEditMode() GuiCollectionSystemsOptions::~GuiCollectionSystemsOptions() { - } void GuiCollectionSystemsOptions::addSystemsToMenu() { + std::map autoSystems = + CollectionSystemManager::get()->getAutoCollectionSystems(); - std::map autoSystems = CollectionSystemManager::get()->getAutoCollectionSystems(); + autoOptionList = + std::make_shared< OptionListComponent >( + mWindow, + _("SELECT COLLECTIONS"), + true); - autoOptionList = std::make_shared< OptionListComponent >(mWindow, "SELECT COLLECTIONS", true); - - // add Auto Systems - for(std::map::const_iterator it = autoSystems.cbegin() ; it != autoSystems.cend() ; it++ ) + // añadir sistemas automáticos (ALL GAMES, FAVORITES, RANDOM, LAST PLAYED) con traducción .ini + for (auto it = autoSystems.cbegin(); it != autoSystems.cend(); ++it) { - autoOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); + // longName viene como "ALL GAMES", "FAVORITES", etc. + std::string label = it->second.decl.longName; + + // Las claves en el .ini están en minúsculas: "all games", "favorites", "random", "last played" + std::string lowerKey = Utils::String::toLower(label); + std::string translated = LocaleES::getInstance().translate(lowerKey); + + if (translated != lowerKey) + label = translated; // usar la traducción si existe + + autoOptionList->add( + label, + it->second.decl.name, + it->second.isEnabled); } - mMenu.addWithLabel("AUTOMATIC GAME COLLECTIONS", autoOptionList); + mMenu.addWithLabel(_("AUTOMATIC GAME COLLECTIONS"), autoOptionList); - std::map customSystems = CollectionSystemManager::get()->getCustomCollectionSystems(); + std::map customSystems = + CollectionSystemManager::get()->getCustomCollectionSystems(); - customOptionList = std::make_shared< OptionListComponent >(mWindow, "SELECT COLLECTIONS", true); + customOptionList = + std::make_shared< OptionListComponent >( + mWindow, + _("SELECT COLLECTIONS"), + true); - // add Custom Systems - for(std::map::const_iterator it = customSystems.cbegin() ; it != customSystems.cend() ; it++ ) + // añadir sistemas personalizados (se muestran con el nombre que les ponga el usuario) + for (auto it = customSystems.cbegin(); it != customSystems.cend(); ++it) { - customOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); + customOptionList->add( + it->second.decl.longName, + it->second.decl.name, + it->second.isEnabled); } - mMenu.addWithLabel("CUSTOM GAME COLLECTIONS", customOptionList); + mMenu.addWithLabel(_("CUSTOM GAME COLLECTIONS"), customOptionList); } void GuiCollectionSystemsOptions::applySettings() @@ -217,19 +320,19 @@ void GuiCollectionSystemsOptions::applySettings() bool prevShow = Settings::getInstance()->getBool("CollectionShowSystemInfo"); bool outShow = toggleSystemNameInCollections->getState(); - // check if custom collection is still enabled for 'collection during screensaver'. - // if not set 'collection during screensaver' to "" which renders as + // comprobar si la colección personalizada sigue habilitada para 'collection during screensaver' + // si no, ponerla en "" (se muestra como ) std::string enabledCollectionName = ""; std::vector selection = defaultScreenSaverCollection->getSelectedObjects(); - if (selection.size() > 0) + if (!selection.empty()) { std::string selectedCollection = selection.at(0); - if (selectedCollection != "") + if (!selectedCollection.empty()) { std::vector enabledCollections = customOptionList->getSelectedObjects(); - for(auto nameIt = enabledCollections.begin(); nameIt != enabledCollections.end(); nameIt++) + for (auto nameIt = enabledCollections.begin(); nameIt != enabledCollections.end(); ++nameIt) { - if(*nameIt == selectedCollection) + if (*nameIt == selectedCollection) { enabledCollectionName = selectedCollection; break; @@ -241,8 +344,12 @@ void GuiCollectionSystemsOptions::applySettings() Settings::getInstance()->setString("DefaultScreenSaverCollection", enabledCollectionName); Settings::getInstance()->setBool("DoublePressRemovesFromFavs", doublePressToRemoveFavs->getState()); - bool needRefreshCollectionSettings = prevAuto != outAuto || prevCustom != outCustom || outSort != prevSort || outBundle != prevBundle - || prevShow != outShow; + bool needRefreshCollectionSettings = + prevAuto != outAuto || + prevCustom != outCustom || + outSort != prevSort || + outBundle != prevBundle || + prevShow != outShow; if (needRefreshCollectionSettings) { @@ -274,21 +381,20 @@ void GuiCollectionSystemsOptions::updateSettings(std::string newAutoSettings, st bool GuiCollectionSystemsOptions::input(InputConfig* config, Input input) { bool consumed = GuiComponent::input(config, input); - if(consumed) + if (consumed) return true; - if(config->isMappedTo("b", input) && input.value != 0) + if (config->isMappedTo("b", input) && input.value != 0) { applySettings(); } - return false; } std::vector GuiCollectionSystemsOptions::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _("BACK"))); return prompts; } diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 436b131c46..9e3ab537e9 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -11,10 +11,16 @@ #include "GuiMetaDataEd.h" #include "SystemData.h" #include "components/TextListComponent.h" - -GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : GuiComponent(window), - mSystem(system), mMenu(window, "OPTIONS"), mFromPlaceholder(false), mFiltersChanged(false), - mJumpToSelected(false), mMetadataChanged(false) +#include "../LocaleES.h" // 🔹 Sistema de traducción + +GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) + : GuiComponent(window) + , mMenu(window, es_translate("OPTIONS").c_str(), Font::get(FONT_SIZE_LARGE)) // 🔹 Título traducible + , mSystem(system) + , mFromPlaceholder(false) + , mFiltersChanged(false) + , mJumpToSelected(false) + , mMetadataChanged(false) { addChild(&mMenu); @@ -23,27 +29,28 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui mFromPlaceholder = file->isPlaceHolder(); ComponentListRow row; - if (!mFromPlaceholder) { - row.elements.clear(); - + if (!mFromPlaceholder) + { std::string currentSort = mSystem->getRootFolder()->getSortDescription(); std::string reqSort = FileSorts::SortTypes.at(0).description; // "jump to letter" menuitem only available (and correct jumping) on sort order "name, asc" - if (currentSort == reqSort) { + if (currentSort == reqSort) + { bool outOfRange = false; char curChar = (char)toupper(getGamelist()->getCursor()->getSortName()[0]); + // define supported character range - // this range includes all numbers, capital letters, and most reasonable symbols char startChar = '!'; char endChar = '_'; - if (curChar < startChar || curChar > endChar) { + if (curChar < startChar || curChar > endChar) + { // most likely 8 bit ASCII or Unicode (Prefix: 0xc2 or 0xe2) value curChar = startChar; outOfRange = true; } - mJumpToLetterList = std::make_shared(mWindow, "JUMP TO ...", false); + mJumpToLetterList = std::make_shared(mWindow, es_translate("JUMP TO ..."), false); for (char c = startChar; c <= endChar; c++) { // check if c is a valid first letter in current list @@ -60,15 +67,21 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui } } - row.addElement(std::make_shared(mWindow, "JUMP TO ...", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.elements.clear(); + row.addElement(std::make_shared( + mWindow, + es_translate("JUMP TO ..."), + Font::get(FONT_SIZE_MEDIUM), + 0x777777FF), + true); row.addElement(mJumpToLetterList, false); row.input_handler = [&](InputConfig* config, Input input) { - if(config->isMappedTo("a", input) && input.value) + if (config->isMappedTo("a", input) && input.value) { jumpToLetter(); return true; } - else if(mJumpToLetterList->input(config, input)) + else if (mJumpToLetterList->input(config, input)) { return true; } @@ -79,74 +92,115 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui // add launch system screensaver std::string screensaver_behavior = Settings::getInstance()->getString("ScreenSaverBehavior"); - bool useGamelistMedia = screensaver_behavior == "random video" || (screensaver_behavior == "slideshow" && !Settings::getInstance()->getBool("SlideshowScreenSaverCustomMediaSource")); + bool useGamelistMedia = screensaver_behavior == "random video" || + (screensaver_behavior == "slideshow" && !Settings::getInstance()->getBool("SlideshowScreenSaverCustomMediaSource")); + bool rpConfigSelected = "retropie" == mSystem->getName(); bool collectionsSelected = mSystem->getName() == CollectionSystemManager::get()->getCustomCollectionsBundle()->getName(); - if (!rpConfigSelected && useGamelistMedia && (!collectionsSelected || collectionsSelected && file->getType() == GAME)) { + if (!rpConfigSelected && useGamelistMedia && (!collectionsSelected || (collectionsSelected && file->getType() == GAME))) + { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "LAUNCH SYSTEM SCREENSAVER", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared( + mWindow, + es_translate("LAUNCH SYSTEM SCREENSAVER"), + Font::get(FONT_SIZE_MEDIUM), + 0x777777FF), + true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::launchSystemScreenSaver, this)); mMenu.addRow(row); } // "sort list by" menuitem - mListSort = std::make_shared(mWindow, "SORT GAMES BY", false); - for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) + mListSort = std::make_shared(mWindow, es_translate("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, sort.description == currentSort); } - mMenu.addWithLabel("SORT GAMES BY", mListSort); - + mMenu.addWithLabel(es_translate("SORT GAMES BY"), mListSort); } // show filtered menu - if(!Settings::getInstance()->getBool("ForceDisableFilters")) + if (!Settings::getInstance()->getBool("ForceDisableFilters")) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FILTER GAMELIST", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared( + mWindow, + es_translate("FILTER GAMELIST"), + Font::get(FONT_SIZE_MEDIUM), + 0x777777FF), + true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); mMenu.addRow(row); } - std::map customCollections = CollectionSystemManager::get()->getCustomCollectionSystems(); + std::map customCollections = + CollectionSystemManager::get()->getCustomCollectionSystems(); - if(UIModeController::getInstance()->isUIModeFull() && - ((customCollections.find(system->getName()) != customCollections.cend() && CollectionSystemManager::get()->getEditingCollection() != system->getName()) || - CollectionSystemManager::get()->getCustomCollectionsBundle()->getName() == system->getName())) + if (UIModeController::getInstance()->isUIModeFull() && + ((customCollections.find(system->getName()) != customCollections.cend() && + CollectionSystemManager::get()->getEditingCollection() != system->getName()) || + CollectionSystemManager::get()->getCustomCollectionsBundle()->getName() == system->getName())) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "ADD/REMOVE GAMES TO THIS GAME COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared( + mWindow, + es_translate("ADD/REMOVE GAMES TO THIS GAME COLLECTION"), + Font::get(FONT_SIZE_MEDIUM), + 0x777777FF), + true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::startEditMode, this)); mMenu.addRow(row); } - if(UIModeController::getInstance()->isUIModeFull() && CollectionSystemManager::get()->isEditing()) + if (UIModeController::getInstance()->isUIModeFull() && CollectionSystemManager::get()->isEditing()) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + std::string finishText = es_translate("FINISH EDITING") + " '" + + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + + "' " + es_translate("COLLECTION"); + row.addElement(std::make_shared( + mWindow, + finishText, + Font::get(FONT_SIZE_MEDIUM), + 0x777777FF), + true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::exitEditMode, this)); mMenu.addRow(row); } - if(UIModeController::getInstance()->isUIModeFull() && system == CollectionSystemManager::get()->getRandomCollection()) + if (UIModeController::getInstance()->isUIModeFull() && system == CollectionSystemManager::get()->getRandomCollection()) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "GET NEW RANDOM GAMES", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared( + mWindow, + es_translate("GET NEW RANDOM GAMES"), + Font::get(FONT_SIZE_MEDIUM), + 0x777777FF), + true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::recreateCollection, this)); mMenu.addRow(row); } - if (UIModeController::getInstance()->isUIModeFull() && !mFromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) + if (UIModeController::getInstance()->isUIModeFull() && !mFromPlaceholder && + !(mSystem->isCollection() && file->getType() == FOLDER)) { row.elements.clear(); - std::string lblTxt = std::string("EDIT THIS "); - lblTxt += std::string((file->getType() == FOLDER ? "FOLDER" : "GAME")); - lblTxt += std::string("'S METADATA"); - row.addElement(std::make_shared(mWindow, lblTxt, Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + std::string lblTxt; + if (file->getType() == FOLDER) + lblTxt = es_translate("EDIT THIS FOLDER'S METADATA"); + else + lblTxt = es_translate("EDIT THIS GAME'S METADATA"); + + row.addElement(std::make_shared( + mWindow, + lblTxt, + Font::get(FONT_SIZE_MEDIUM), + 0x777777FF), + true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openMetaDataEd, this)); mMenu.addRow(row); @@ -154,32 +208,36 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui // center the menu setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); + mMenu.setPosition( + (mSize.x() - mMenu.getSize().x()) / 2, + (mSize.y() - mMenu.getSize().y()) / 2); } GuiGamelistOptions::~GuiGamelistOptions() { FileData* root = mSystem->getRootFolder(); // apply sort - if (!mFromPlaceholder) { - const FileData::SortType selectedSort = mJumpToSelected ? FileSorts::SortTypes.at(0) /* force "name, asc" */ : *mListSort->getSelected(); - if (root->getSortDescription() != selectedSort.description) { + if (!mFromPlaceholder) + { + const FileData::SortType selectedSort = + mJumpToSelected + ? FileSorts::SortTypes.at(0) // force "name, asc" + : *mListSort->getSelected(); + + if (root->getSortDescription() != selectedSort.description) + { root->sort(selectedSort); // will also recursively sort children - // notify that the root folder was sorted getGamelist()->onFileChanged(root, FILE_SORTED); } } if (mFiltersChanged || mMetadataChanged) { - // force refresh of cursor list position - ViewController::get()->getGameListView(mSystem)->setViewportTop(TextListComponent::REFRESH_LIST_CURSOR_POS); - // re-display the elements for whatever new or renamed game is selected + ViewController::get()->getGameListView(mSystem)->setViewportTop( + TextListComponent::REFRESH_LIST_CURSOR_POS); ViewController::get()->reloadGameListView(mSystem); - if (mFiltersChanged) { - // trigger repaint of cursor and list detail + if (mFiltersChanged) getGamelist()->onFileChanged(root, FILE_SORTED); - } } } @@ -187,13 +245,13 @@ bool GuiGamelistOptions::launchSystemScreenSaver() { SystemData* system = mSystem; std::string systemName = system->getName(); - // need to check if we're in a folder inside the collections bundle, to launch from there - if(systemName == CollectionSystemManager::get()->getCustomCollectionsBundle()->getName()) + + if (systemName == CollectionSystemManager::get()->getCustomCollectionsBundle()->getName()) { FileData* file = getGamelist()->getCursor(); // is GAME otherwise menuentry would have been hidden - // we are inside a specific collection. We want to launch for that one. system = file->getSystem(); } + mWindow->startScreenSaver(system); mWindow->renderScreenSaver(); @@ -217,20 +275,14 @@ void GuiGamelistOptions::recreateCollection() void GuiGamelistOptions::startEditMode() { std::string editingSystem = mSystem->getName(); - // need to check if we're editing the collections bundle, as we will want to edit the selected collection within - if(editingSystem == CollectionSystemManager::get()->getCustomCollectionsBundle()->getName()) + + if (editingSystem == CollectionSystemManager::get()->getCustomCollectionsBundle()->getName()) { FileData* file = getGamelist()->getCursor(); - // do we have the cursor on a specific collection? if (file->getType() == FOLDER) - { editingSystem = file->getName(); - } else - { - // we are inside a specific collection. We want to edit that one. editingSystem = file->getSystem()->getName(); - } } CollectionSystemManager::get()->setEditMode(editingSystem); delete this; @@ -244,8 +296,6 @@ void GuiGamelistOptions::exitEditMode() void GuiGamelistOptions::openMetaDataEd() { - // open metadata editor - // get the FileData that hosts the original metadata FileData* file = getGamelist()->getCursor()->getSourceFileData(); ScraperSearchParams p; p.game = file; @@ -271,7 +321,14 @@ void GuiGamelistOptions::openMetaDataEd() }; } - mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(), p, Utils::FileSystem::getFileName(file->getPath()), saveBtnFunc, deleteBtnFunc)); + mWindow->pushGui(new GuiMetaDataEd( + mWindow, + &file->metadata, + file->metadata.getMDD(), + p, + Utils::FileSystem::getFileName(file->getPath()), + saveBtnFunc, + deleteBtnFunc)); } void GuiGamelistOptions::jumpToLetter() @@ -279,34 +336,33 @@ void GuiGamelistOptions::jumpToLetter() char letter = mJumpToLetterList->getSelected(); IGameListView* gamelist = getGamelist(); - // this is a really shitty way to get a list of files - const std::vector& files = gamelist->getCursor()->getParent()->getChildrenListToDisplay(); + const std::vector& files = + gamelist->getCursor()->getParent()->getChildrenListToDisplay(); long min = 0; long max = (long)files.size() - 1; long mid = 0; - while(max >= min) + while (max >= min) { mid = ((max - min) / 2) + min; - // game somehow has no first character to check - if(files.at(mid)->getName().empty()) + if (files.at(mid)->getName().empty()) continue; char checkLetter = (char)toupper(files.at(mid)->getSortName()[0]); - if(checkLetter < letter) + if (checkLetter < letter) min = mid + 1; - else if(checkLetter > letter || (mid > 0 && (letter == toupper(files.at(mid - 1)->getSortName()[0])))) + else if (checkLetter > letter || + (mid > 0 && (letter == toupper(files.at(mid - 1)->getSortName()[0])))) max = mid - 1; else - break; //exact match found + break; // exact match found } gamelist->setCursor(files.at(mid)); - // flag to force default sort order "name, asc", if user changed the sortorder in the options dialog mJumpToSelected = true; delete this; @@ -314,7 +370,7 @@ void GuiGamelistOptions::jumpToLetter() bool GuiGamelistOptions::input(InputConfig* config, Input input) { - if((config->isMappedTo("b", input) || config->isMappedTo("select", input)) && input.value) + if ((config->isMappedTo("b", input) || config->isMappedTo("select", input)) && input.value) { delete this; return true; @@ -333,7 +389,7 @@ HelpStyle GuiGamelistOptions::getHelpStyle() std::vector GuiGamelistOptions::getHelpPrompts() { auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "close")); + prompts.push_back(HelpPrompt("b", es_translate("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiGamelistOptions.h b/es-app/src/guis/GuiGamelistOptions.h index dd14399b54..fcae98d335 100644 --- a/es-app/src/guis/GuiGamelistOptions.h +++ b/es-app/src/guis/GuiGamelistOptions.h @@ -7,6 +7,9 @@ #include "FileData.h" #include "GuiComponent.h" +// 🔹 Ruta correcta +#include "LocaleESHook.h" + class IGameListView; class SystemData; diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index b3150db4a5..41541919ab 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -9,6 +9,7 @@ #include "guis/GuiMsgBox.h" #include "guis/GuiScraperStart.h" #include "guis/GuiSettings.h" +#include "guis/GuiThemeOptions.h" #include "views/UIModeController.h" #include "views/ViewController.h" #include "CollectionSystemManager.h" @@ -18,27 +19,102 @@ #include "VolumeControl.h" #include #include +#include // system() + +#include "PowerSaver.h" #include "platform.h" #include "FileSorts.h" #include "views/gamelist/IGameListView.h" #include "guis/GuiInfoPopup.h" +#include "Settings.h" +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" +#include "resources/Font.h" +#include "ThemeData.h" +#include "LocaleES.h" + +// Navegación por sonido +#include "Sound.h" +#include "NavigationSounds.h" +#include + +// Música de fondo ES-X (gestor central) +#include "audio/BackgroundMusicManager.h" + +// Helper local para traducciones .ini y colores de menú +namespace +{ + inline std::string _(const std::string& key) + { + return LocaleES::getInstance().translate(key); + } -GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, "MAIN MENU"), mVersion(window) + inline unsigned int getMenuTextColor() + { + // Si MenuDark = true → texto claro; si no → gris clásico + return Settings::getInstance()->getBool("MenuDark") ? 0xFFFFFFFF : 0x777777FF; + } + + inline unsigned int getVersionTextColor() + { + // Versión un poco más clara en modo oscuro + return Settings::getInstance()->getBool("MenuDark") ? 0xB0B0B0FF : 0x5E5E5EFF; + } + + // === SONIDOS DE NAVEGACIÓN PARA MENÚ (BACK, ETC.) === + + inline std::shared_ptr getNavSound(const std::string& logicalName) + { + auto vcState = ViewController::get()->getState(); + SystemData* sys = vcState.getSystem(); + if (!sys) + return nullptr; + + const std::shared_ptr& theme = sys->getTheme(); + if (!theme) + return nullptr; + + return NavigationSounds::getFromTheme(theme, logicalName); + } + + inline void playMenuBackSound() + { + // esquema tipo Batocera: "back" + auto snd = getNavSound("back"); + if (snd) + snd->play(); + } +} + +GuiMenu::GuiMenu(Window* window) + : GuiComponent(window) + , mMenu(window, "MAIN MENU") + , mVersion(window) { + // Cargar idioma actual antes de crear las entradas + LocaleES::getInstance().loadFromSettings(); + + // Título del menú + mMenu.setTitle(_("MAIN MENU").c_str(), Font::get(FONT_SIZE_LARGE)); + bool isFullUI = UIModeController::getInstance()->isUIModeFull(); + unsigned int menuColor = getMenuTextColor(); - if (isFullUI) { - addEntry("SCRAPER", 0x777777FF, true, [this] { openScraperSettings(); }); - addEntry("SOUND SETTINGS", 0x777777FF, true, [this] { openSoundSettings(); }); - addEntry("UI SETTINGS", 0x777777FF, true, [this] { openUISettings(); }); - addEntry("GAME COLLECTION SETTINGS", 0x777777FF, true, [this] { openCollectionSystemSettings(); }); - addEntry("OTHER SETTINGS", 0x777777FF, true, [this] { openOtherSettings(); }); - addEntry("CONFIGURE INPUT", 0x777777FF, true, [this] { openConfigInput(); }); - } else { - addEntry("SOUND SETTINGS", 0x777777FF, true, [this] { openSoundSettings(); }); + if (isFullUI) + { + addEntry(_("SCRAPER").c_str(), menuColor, true, [this] { openScraperSettings(); }); + addEntry(_("SOUND SETTINGS").c_str(), menuColor, true, [this] { openSoundSettings(); }); + addEntry(_("UI SETTINGS").c_str(), menuColor, true, [this] { openUISettings(); }); + addEntry(_("GAME COLLECTION SETTINGS").c_str(), menuColor, true, [this] { openCollectionSystemSettings(); }); + addEntry(_("OTHER SETTINGS").c_str(), menuColor, true, [this] { openOtherSettings(); }); + addEntry(_("CONFIGURE INPUT").c_str(), menuColor, true, [this] { openConfigInput(); }); + } + else + { + addEntry(_("SOUND SETTINGS").c_str(), menuColor, true, [this] { openSoundSettings(); }); } - addEntry("QUIT", 0x777777FF, true, [this] {openQuitMenu(); }); + addEntry(_("QUIT").c_str(), menuColor, true, [this] { openQuitMenu(); }); addChild(&mMenu); addVersionInfo(); @@ -48,33 +124,39 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, "MAIN MEN void GuiMenu::openScraperSettings() { - auto s = new GuiSettings(mWindow, "SCRAPER"); + auto s = new GuiSettings(mWindow, _("SCRAPER").c_str()); // scrape from - auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, "SCRAPE FROM", false); + auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, _("SCRAPE FROM").c_str(), false); std::vector scrapers = getScraperList(); - // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. - for(auto it = scrapers.cbegin(); it != scrapers.cend(); it++) + for (auto it = scrapers.cbegin(); it != scrapers.cend(); 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()); }); + s->addWithLabel(_("SCRAPE FROM").c_str(), scraper_list); + s->addSaveFunc([scraper_list] { + Settings::getInstance()->setString("Scraper", scraper_list->getSelected()); + }); // scrape ratings auto scrape_ratings = std::make_shared(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()); }); + s->addWithLabel(_("SCRAPE RATINGS").c_str(), scrape_ratings); + s->addSaveFunc([scrape_ratings] { + Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); + }); // scrape now ComponentListRow row; auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; std::function openAndSave = openScrapeNow; - openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; + openAndSave = [s, openAndSave] { + s->save(); + openAndSave(); + }; row.makeAcceptInputHandler(openAndSave); - auto scrape_now = std::make_shared(mWindow, "SCRAPE NOW", Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + auto scrape_now = std::make_shared(mWindow, _("SCRAPE NOW"), Font::get(FONT_SIZE_MEDIUM), getMenuTextColor()); auto bracket = makeArrow(mWindow); row.addElement(scrape_now, true); row.addElement(bracket, false); @@ -85,19 +167,37 @@ void GuiMenu::openScraperSettings() void GuiMenu::openSoundSettings() { - auto s = new GuiSettings(mWindow, "SOUND SETTINGS"); + auto s = new GuiSettings(mWindow, _("SOUND SETTINGS").c_str()); // volume auto volume = std::make_shared(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)Math::round(volume->getValue())); }); + s->addWithLabel(_("SYSTEM VOLUME").c_str(), volume); + s->addSaveFunc([volume] { + VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); + }); if (UIModeController::getInstance()->isUIModeFull()) { + // 🎵 MÚSICA DE FONDO (ES-X) – usar estado REAL del BackgroundMusicManager + { + auto bgm_switch = std::make_shared(mWindow); + + // Estado inicial: lo que dice el manager (no Settings suelto) + bgm_switch->setState(BackgroundMusicManager::getInstance().isEnabled()); + + s->addWithLabel(_("BACKGROUND MUSIC").c_str(), bgm_switch); + s->addSaveFunc([bgm_switch] { + BackgroundMusicManager::getInstance().setEnabled(bgm_switch->getState()); + + // (Opcional) mantener compatibilidad con una key vieja si la usaste antes: + Settings::getInstance()->setBool("EnableBGM", bgm_switch->getState()); + }); + } + #if defined(__linux__) // audio card - auto audio_card = std::make_shared< OptionListComponent >(mWindow, "AUDIO CARD", false); + auto audio_card = std::make_shared< OptionListComponent >(mWindow, _("AUDIO CARD").c_str(), false); std::vector audio_cards; audio_cards.push_back("default"); audio_cards.push_back("sysdefault"); @@ -105,14 +205,14 @@ void GuiMenu::openSoundSettings() audio_cards.push_back("hw"); audio_cards.push_back("plughw"); audio_cards.push_back("null"); - if (Settings::getInstance()->getString("AudioCard") != "") { - if(std::find(audio_cards.begin(), audio_cards.end(), Settings::getInstance()->getString("AudioCard")) == audio_cards.end()) { + if (Settings::getInstance()->getString("AudioCard") != "") + { + if (std::find(audio_cards.begin(), audio_cards.end(), Settings::getInstance()->getString("AudioCard")) == audio_cards.end()) audio_cards.push_back(Settings::getInstance()->getString("AudioCard")); - } } - for(auto ac = audio_cards.cbegin(); ac != audio_cards.cend(); ac++) + for (auto ac = audio_cards.cbegin(); ac != audio_cards.cend(); ac++) audio_card->add(*ac, *ac, Settings::getInstance()->getString("AudioCard") == *ac); - s->addWithLabel("AUDIO CARD", audio_card); + s->addWithLabel(_("AUDIO CARD").c_str(), audio_card); s->addSaveFunc([audio_card] { Settings::getInstance()->setString("AudioCard", audio_card->getSelected()); VolumeControl::getInstance()->deinit(); @@ -120,7 +220,7 @@ void GuiMenu::openSoundSettings() }); // volume control device - auto vol_dev = std::make_shared< OptionListComponent >(mWindow, "AUDIO DEVICE", false); + auto vol_dev = std::make_shared< OptionListComponent >(mWindow, _("AUDIO DEVICE").c_str(), false); std::vector transitions; transitions.push_back("PCM"); transitions.push_back("HDMI"); @@ -129,14 +229,14 @@ void GuiMenu::openSoundSettings() transitions.push_back("Master"); transitions.push_back("Digital"); transitions.push_back("Analogue"); - if (Settings::getInstance()->getString("AudioDevice") != "") { - if(std::find(transitions.begin(), transitions.end(), Settings::getInstance()->getString("AudioDevice")) == transitions.end()) { + if (Settings::getInstance()->getString("AudioDevice") != "") + { + if (std::find(transitions.begin(), transitions.end(), Settings::getInstance()->getString("AudioDevice")) == transitions.end()) transitions.push_back(Settings::getInstance()->getString("AudioDevice")); - } } - for(auto it = transitions.cbegin(); it != transitions.cend(); it++) + for (auto it = transitions.cbegin(); it != transitions.cend(); it++) vol_dev->add(*it, *it, Settings::getInstance()->getString("AudioDevice") == *it); - s->addWithLabel("AUDIO DEVICE", vol_dev); + s->addWithLabel(_("AUDIO DEVICE").c_str(), vol_dev); s->addSaveFunc([vol_dev] { Settings::getInstance()->setString("AudioDevice", vol_dev->getSelected()); VolumeControl::getInstance()->deinit(); @@ -147,7 +247,7 @@ void GuiMenu::openSoundSettings() // disable sounds auto sounds_enabled = std::make_shared(mWindow); sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); - s->addWithLabel("ENABLE NAVIGATION SOUNDS", sounds_enabled); + s->addWithLabel(_("ENABLE NAVIGATION SOUNDS").c_str(), sounds_enabled); s->addSaveFunc([sounds_enabled] { if (sounds_enabled->getState() && !Settings::getInstance()->getBool("EnableSounds") @@ -161,28 +261,30 @@ void GuiMenu::openSoundSettings() auto video_audio = std::make_shared(mWindow); video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); - s->addWithLabel("ENABLE VIDEO AUDIO", video_audio); - s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); + s->addWithLabel(_("ENABLE VIDEO AUDIO").c_str(), video_audio); + s->addSaveFunc([video_audio] { + Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); + }); #ifdef _OMX_ // OMX player Audio Device - auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, "OMX PLAYER AUDIO DEVICE", false); + auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, _("OMX PLAYER AUDIO DEVICE").c_str(), false); std::vector omx_cards; - // RPi Specific Audio Cards omx_cards.push_back("local"); omx_cards.push_back("hdmi"); omx_cards.push_back("both"); omx_cards.push_back("alsa"); omx_cards.push_back("alsa:hw:0,0"); omx_cards.push_back("alsa:hw:1,0"); - if (Settings::getInstance()->getString("OMXAudioDev") != "") { - if (std::find(omx_cards.begin(), omx_cards.end(), Settings::getInstance()->getString("OMXAudioDev")) == omx_cards.end()) { + omx_cards.push_back("alsa:hw:1,0"); + if (Settings::getInstance()->getString("OMXAudioDev") != "") + { + if (std::find(omx_cards.begin(), omx_cards.end(), Settings::getInstance()->getString("OMXAudioDev")) == omx_cards.end()) omx_cards.push_back(Settings::getInstance()->getString("OMXAudioDev")); - } } for (auto it = omx_cards.cbegin(); it != omx_cards.cend(); it++) omx_audio_dev->add(*it, *it, Settings::getInstance()->getString("OMXAudioDev") == *it); - s->addWithLabel("OMX PLAYER AUDIO DEVICE", omx_audio_dev); + s->addWithLabel(_("OMX PLAYER AUDIO DEVICE").c_str(), omx_audio_dev); s->addSaveFunc([omx_audio_dev] { if (Settings::getInstance()->getString("OMXAudioDev") != omx_audio_dev->getSelected()) Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); @@ -191,57 +293,57 @@ void GuiMenu::openSoundSettings() } mWindow->pushGui(s); - } void GuiMenu::openUISettings() { - auto s = new GuiSettings(mWindow, "UI SETTINGS"); + auto s = new GuiSettings(mWindow, _("UI SETTINGS").c_str()); - //UI mode - auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); + // UI mode + auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, _("UI MODE").c_str(), false); std::vector UImodes = UIModeController::getInstance()->getUIModes(); for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); - s->addWithLabel("UI MODE", UImodeSelection); Window* window = mWindow; - s->addSaveFunc([ UImodeSelection, window] + s->addSaveFunc([UImodeSelection, window] { std::string selectedMode = UImodeSelection->getSelected(); if (selectedMode != "Full") { - std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; - msg += "This will hide most menu-options to prevent changes to the system.\n"; - msg += "To unlock and return to the full UI, enter this code: \n"; + std::string msg = _("You are changing the UI to a restricted mode:") + "\n" + selectedMode + "\n"; + msg += _("This will hide most menu-options to prevent changes to the system.") + "\n"; + msg += _("To unlock and return to the full UI, enter this code:") + " \n"; msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; - msg += "Do you want to proceed?"; + msg += _("Do you want to proceed?"); window->pushGui(new GuiMsgBox(window, msg, - "YES", [selectedMode] { + _("YES").c_str(), [selectedMode] { LOG(LogDebug) << "Setting UI mode to " << selectedMode; Settings::getInstance()->setString("UIMode", selectedMode); Settings::getInstance()->saveFile(); - }, "NO",nullptr)); + }, _("NO").c_str(), nullptr)); } }); // screensaver ComponentListRow screensaver_row; screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, "SCREENSAVER SETTINGS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + screensaver_row.addElement(std::make_shared(mWindow, _("SCREENSAVER SETTINGS"), Font::get(FONT_SIZE_MEDIUM), getMenuTextColor()), true); screensaver_row.addElement(makeArrow(mWindow), false); screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); s->addRow(screensaver_row); - // quick system select (left/right in game list view) + // quick system select auto quick_sys_select = std::make_shared(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()); }); + s->addWithLabel(_("QUICK SYSTEM SELECT").c_str(), quick_sys_select); + s->addSaveFunc([quick_sys_select] { + Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); + }); - // carousel transition option + // carousel transitions auto move_carousel = std::make_shared(mWindow); move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); - s->addWithLabel("CAROUSEL TRANSITIONS", move_carousel); + s->addWithLabel(_("CAROUSEL TRANSITIONS").c_str(), move_carousel); s->addSaveFunc([move_carousel] { if (move_carousel->getState() && !Settings::getInstance()->getBool("MoveCarousel") @@ -254,14 +356,14 @@ void GuiMenu::openUISettings() }); // transition style - auto transition_style = std::make_shared< OptionListComponent >(mWindow, "TRANSITION STYLE", false); + auto transition_style = std::make_shared< OptionListComponent >(mWindow, _("TRANSITION STYLE").c_str(), false); std::vector transitions; transitions.push_back("fade"); transitions.push_back("slide"); transitions.push_back("instant"); - for(auto it = transitions.cbegin(); it != transitions.cend(); it++) + for (auto it = transitions.cbegin(); it != transitions.cend(); it++) transition_style->add(*it, *it, Settings::getInstance()->getString("TransitionStyle") == *it); - s->addWithLabel("TRANSITION STYLE", transition_style); + s->addWithLabel(_("TRANSITION STYLE").c_str(), transition_style); s->addSaveFunc([transition_style] { if (Settings::getInstance()->getString("TransitionStyle") == "instant" && transition_style->getSelected() != "instant" @@ -273,41 +375,121 @@ void GuiMenu::openUISettings() Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); }); + // DARK MENU (modo oscuro para menús) + { + auto menu_dark = std::make_shared(mWindow); + menu_dark->setState(Settings::getInstance()->getBool("MenuDark")); + s->addWithLabel(_("DARK MENU").c_str(), menu_dark); + s->addSaveFunc([menu_dark] { + Settings::getInstance()->setBool("MenuDark", menu_dark->getState()); + }); + } + + // ============================================================ + // CLOCK (reloj global overlay) - ON/OFF + // key: ShowClock + // ============================================================ + { + auto show_clock = std::make_shared(mWindow); + show_clock->setState(Settings::getInstance()->getBool("ShowClock")); + s->addWithLabel(_("CLOCK").c_str(), show_clock); + s->addSaveFunc([show_clock] { + Settings::getInstance()->setBool("ShowClock", show_clock->getState()); + }); + } + // theme set auto themeSets = ThemeData::getThemeSets(); - if(!themeSets.empty()) + if (!themeSets.empty()) { std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(selectedSet == themeSets.cend()) + if (selectedSet == themeSets.cend()) selectedSet = themeSets.cbegin(); - auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME SET", false); - for(auto it = themeSets.cbegin(); it != themeSets.cend(); it++) + auto theme_set = std::make_shared< OptionListComponent >(mWindow, _("THEME SET").c_str(), false); + for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) theme_set->add(it->first, it->first, it == selectedSet); - s->addWithLabel("THEME SET", theme_set); + s->addWithLabel(_("THEME SET").c_str(), theme_set); - Window* window = mWindow; - s->addSaveFunc([window, theme_set] + Window* window2 = mWindow; + s->addSaveFunc([window2, theme_set] { bool needReload = false; std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - if(oldTheme != theme_set->getSelected()) + if (oldTheme != theme_set->getSelected()) needReload = true; Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - if(needReload) + if (needReload) { Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->reloadAll(true); // TODO - replace this with some sort of signal-based implementation + ViewController::get()->reloadAll(true); } }); } + // THEME OPTIONS (GUI interna de ES-X) + { + ComponentListRow theme_row; + theme_row.elements.clear(); + theme_row.addElement(std::make_shared(mWindow, _("THEME OPTIONS"), Font::get(FONT_SIZE_MEDIUM), getMenuTextColor()), true); + theme_row.addElement(makeArrow(mWindow), false); + theme_row.makeAcceptInputHandler(std::bind(&GuiMenu::openThemeOptions, this)); + s->addRow(theme_row); + } + + // LANGUAGE (usando .ini en ~/.emulationstation/lang) + { + auto language_list = std::make_shared< OptionListComponent >(mWindow, _("LANGUAGE").c_str(), false); + + std::string currentLang = Settings::getInstance()->getString("Language"); + if (currentLang.empty()) + currentLang = "en"; + + std::vector languages; + languages.push_back("en"); + languages.push_back("es"); + + std::string langDir = Utils::FileSystem::getHomePath() + "/.emulationstation/lang"; + if (Utils::FileSystem::isDirectory(langDir)) + { + Utils::FileSystem::stringList files = Utils::FileSystem::getDirContent(langDir); + for (auto it = files.cbegin(); it != files.cend(); ++it) + { + if (Utils::FileSystem::isRegularFile(*it) && Utils::FileSystem::getExtension(*it) == ".ini") + { + std::string code = Utils::FileSystem::getStem(*it); + if (std::find(languages.begin(), languages.end(), code) == languages.end()) + languages.push_back(code); + } + } + } + + for (auto &code : languages) + { + std::string label; + if (code == "en") + label = "English"; + else if (code == "es") + label = "Español"; + else + label = Utils::String::toUpper(code); + + language_list->add(label, code, (currentLang == code)); + } + + s->addWithLabel(_("LANGUAGE").c_str(), language_list); + s->addSaveFunc([language_list] { + Settings::getInstance()->setString("Language", language_list->getSelected()); + LocaleES::getInstance().loadFromSettings(); + }); + } + // GameList view style - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, "GAMELIST VIEW STYLE", false); + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE").c_str(), false); std::vector styles; styles.push_back("automatic"); styles.push_back("basic"); @@ -317,7 +499,7 @@ void GuiMenu::openUISettings() for (auto it = styles.cbegin(); it != styles.cend(); it++) gamelist_style->add(*it, *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); - s->addWithLabel("GAMELIST VIEW STYLE", gamelist_style); + s->addWithLabel(_("GAMELIST VIEW STYLE").c_str(), gamelist_style); s->addSaveFunc([gamelist_style] { bool needReload = false; if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) @@ -327,43 +509,38 @@ void GuiMenu::openUISettings() ViewController::get()->reloadAll(); }); - // Optionally ignore leading articles when sorting game titles + // Ignore articles auto ignore_articles = std::make_shared(mWindow); ignore_articles->setState(Settings::getInstance()->getBool("IgnoreLeadingArticles")); - s->addWithLabel("IGNORE ARTICLES (NAME SORT ONLY)", ignore_articles); + s->addWithLabel(_("IGNORE ARTICLES (NAME SORT ONLY)").c_str(), ignore_articles); s->addSaveFunc([ignore_articles, window] { bool articles_are_ignored = Settings::getInstance()->getBool("IgnoreLeadingArticles"); Settings::getInstance()->setBool("IgnoreLeadingArticles", ignore_articles->getState()); if (ignore_articles->getState() != articles_are_ignored) { - //For each system... for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { - //Apply sort recursively FileData* root = (*it)->getRootFolder(); root->sort(getSortTypeFromString(root->getSortName())); - - //Notify that the root folder was sorted ViewController::get()->getGameListView((*it))->onFileChanged(root, FILE_SORTED); } - //Display popup to inform user - GuiInfoPopup* popup = new GuiInfoPopup(window, "Files sorted", 4000); + GuiInfoPopup* popup = new GuiInfoPopup(window, _("FILES SORTED"), 4000); window->setInfoPopup(popup); } }); - // lb/rb uses full screen size paging instead of -10/+10 steps + // full screen paging auto use_fullscreen_paging = std::make_shared(mWindow); use_fullscreen_paging->setState(Settings::getInstance()->getBool("UseFullscreenPaging")); - s->addWithLabel("USE FULL SCREEN PAGING FOR LB/RB", use_fullscreen_paging); + s->addWithLabel(_("USE FULL SCREEN PAGING FOR LB/RB").c_str(), use_fullscreen_paging); s->addSaveFunc([use_fullscreen_paging] { Settings::getInstance()->setBool("UseFullscreenPaging", use_fullscreen_paging->getState()); }); - // Optionally start in selected system - auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, "START ON SYSTEM", false); - systemfocus_list->add("NONE", "", Settings::getInstance()->getString("StartupSystem") == ""); + // startup system + auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _("START ON SYSTEM").c_str(), false); + systemfocus_list->add(_("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { if ("retropie" != (*it)->getName()) @@ -371,7 +548,7 @@ void GuiMenu::openUISettings() systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); } } - s->addWithLabel("START ON SYSTEM", systemfocus_list); + s->addWithLabel(_("START ON SYSTEM").c_str(), systemfocus_list); s->addSaveFunc([systemfocus_list] { Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); }); @@ -379,41 +556,47 @@ void GuiMenu::openUISettings() // show help auto show_help = std::make_shared(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()); }); + s->addWithLabel(_("ON-SCREEN HELP").c_str(), show_help); + s->addSaveFunc([show_help] { + Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); + }); - // enable filters (ForceDisableFilters) + // enable filters auto enable_filter = std::make_shared(mWindow); enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); - s->addWithLabel("ENABLE FILTERS", enable_filter); + s->addWithLabel(_("ENABLE FILTERS").c_str(), enable_filter); s->addSaveFunc([enable_filter] { bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); - if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); + if (enable_filter->getState() != filter_is_enabled) + ViewController::get()->ReloadAndGoToStart(); }); - // hide start menu in Kid Mode + // disable start menu in kid mode auto disable_start = std::make_shared(mWindow); disable_start->setState(Settings::getInstance()->getBool("DisableKidStartMenu")); - s->addWithLabel("DISABLE START MENU IN KID MODE", disable_start); - s->addSaveFunc([disable_start] { Settings::getInstance()->setBool("DisableKidStartMenu", disable_start->getState()); }); + s->addWithLabel(_("DISABLE START MENU IN KID MODE").c_str(), disable_start); + s->addSaveFunc([disable_start] { + Settings::getInstance()->setBool("DisableKidStartMenu", disable_start->getState()); + }); mWindow->pushGui(s); - } void GuiMenu::openOtherSettings() { - auto s = new GuiSettings(mWindow, "OTHER SETTINGS"); + auto s = new GuiSettings(mWindow, _("OTHER SETTINGS").c_str()); // maximum vram auto max_vram = std::make_shared(mWindow, 0.f, 1000.f, 10.f, "Mb"); max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel("VRAM LIMIT", max_vram); - s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); + s->addWithLabel(_("VRAM LIMIT").c_str(), max_vram); + s->addSaveFunc([max_vram] { + Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); + }); // power saver - auto power_saver = std::make_shared< OptionListComponent >(mWindow, "POWER SAVER MODES", false); + auto power_saver = std::make_shared< OptionListComponent >(mWindow, _("POWER SAVER MODES").c_str(), false); std::vector modes; modes.push_back("disabled"); modes.push_back("default"); @@ -421,9 +604,10 @@ void GuiMenu::openOtherSettings() modes.push_back("instant"); for (auto it = modes.cbegin(); it != modes.cend(); it++) power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); - s->addWithLabel("POWER SAVER MODES", power_saver); + s->addWithLabel(_("POWER SAVER MODES").c_str(), power_saver); s->addSaveFunc([this, power_saver] { - if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { + if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") + { Settings::getInstance()->setString("TransitionStyle", "instant"); Settings::getInstance()->setBool("MoveCarousel", false); Settings::getInstance()->setBool("EnableSounds", false); @@ -433,90 +617,92 @@ void GuiMenu::openOtherSettings() }); // gamelists - auto gamelistsSaveMode = std::make_shared< OptionListComponent >(mWindow, "SAVE METADATA", false); + auto gamelistsSaveMode = std::make_shared< OptionListComponent >(mWindow, _("SAVE METADATA").c_str(), false); std::vector saveModes; saveModes.push_back("on exit"); saveModes.push_back("always"); saveModes.push_back("never"); - for(auto it = saveModes.cbegin(); it != saveModes.cend(); it++) + for (auto it = saveModes.cbegin(); it != saveModes.cend(); it++) gamelistsSaveMode->add(*it, *it, Settings::getInstance()->getString("SaveGamelistsMode") == *it); - s->addWithLabel("SAVE METADATA", gamelistsSaveMode); + s->addWithLabel(_("SAVE METADATA").c_str(), gamelistsSaveMode); s->addSaveFunc([gamelistsSaveMode] { Settings::getInstance()->setString("SaveGamelistsMode", gamelistsSaveMode->getSelected()); }); auto parse_gamelists = std::make_shared(mWindow); parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); - s->addWithLabel("PARSE GAMESLISTS ONLY", parse_gamelists); - s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); + s->addWithLabel(_("PARSE GAMESLISTS ONLY").c_str(), parse_gamelists); + s->addSaveFunc([parse_gamelists] { + Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); + }); auto local_art = std::make_shared(mWindow); local_art->setState(Settings::getInstance()->getBool("LocalArt")); - s->addWithLabel("SEARCH FOR LOCAL ART", local_art); - s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); + s->addWithLabel(_("SEARCH FOR LOCAL ART").c_str(), local_art); + s->addSaveFunc([local_art] { + Settings::getInstance()->setBool("LocalArt", local_art->getState()); + }); // hidden files auto hidden_files = std::make_shared(mWindow); hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); - s->addWithLabel("SHOW HIDDEN FILES", hidden_files); - s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); + s->addWithLabel(_("SHOW HIDDEN FILES").c_str(), hidden_files); + s->addSaveFunc([hidden_files] { + Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); + }); #ifdef _OMX_ // Video Player - VideoOmxPlayer auto omx_player = std::make_shared(mWindow); omx_player->setState(Settings::getInstance()->getBool("VideoOmxPlayer")); - s->addWithLabel("USE OMX PLAYER (HW ACCELERATED)", omx_player); + s->addWithLabel(_("USE OMX PLAYER (HW ACCELERATED)").c_str(), omx_player); s->addSaveFunc([omx_player] { - // need to reload all views to re-create the right video components bool needReload = false; - if(Settings::getInstance()->getBool("VideoOmxPlayer") != omx_player->getState()) + if (Settings::getInstance()->getBool("VideoOmxPlayer") != omx_player->getState()) needReload = true; Settings::getInstance()->setBool("VideoOmxPlayer", omx_player->getState()); - if(needReload) + if (needReload) ViewController::get()->reloadAll(); }); - #endif - // hidden files auto background_indexing = std::make_shared(mWindow); background_indexing->setState(Settings::getInstance()->getBool("BackgroundIndexing")); - s->addWithLabel("INDEX FILES DURING SCREENSAVER", background_indexing); - s->addSaveFunc([background_indexing] { Settings::getInstance()->setBool("BackgroundIndexing", background_indexing->getState()); }); + s->addWithLabel(_("INDEX FILES DURING SCREENSAVER").c_str(), background_indexing); + s->addSaveFunc([background_indexing] { + Settings::getInstance()->setBool("BackgroundIndexing", background_indexing->getState()); + }); - // framerate auto framerate = std::make_shared(mWindow); framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); - s->addWithLabel("SHOW FRAMERATE", framerate); - s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); - + s->addWithLabel(_("SHOW FRAMERATE").c_str(), framerate); + s->addSaveFunc([framerate] { + Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); + }); mWindow->pushGui(s); - } void GuiMenu::openConfigInput() { Window* window = mWindow; - window->pushGui(new GuiMsgBox(window, "ARE YOU SURE YOU WANT TO CONFIGURE INPUT?", "YES", + window->pushGui(new GuiMsgBox(window, _("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), + _("YES").c_str(), [window] { - window->pushGui(new GuiDetectDevice(window, false, nullptr)); - }, "NO", nullptr) + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + }, _("NO").c_str(), nullptr) ); - } void GuiMenu::openQuitMenu() { - auto s = new GuiSettings(mWindow, "QUIT"); + auto s = new GuiSettings(mWindow, _("QUIT").c_str()); Window* window = mWindow; - - // command line switch bool confirm_quit = Settings::getInstance()->getBool("ConfirmQuit"); ComponentListRow row; @@ -524,22 +710,26 @@ void GuiMenu::openQuitMenu() { auto static restart_es_fx = []() { Scripting::fireEvent("quit"); - if (quitES(QuitMode::RESTART)) { + if (quitES(QuitMode::RESTART)) + { LOG(LogWarning) << "Restart terminated with non-zero result!"; } }; - if (confirm_quit) { + if (confirm_quit) + { row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", restart_es_fx, "NO", nullptr)); + window->pushGui(new GuiMsgBox(window, _("REALLY RESTART?"), _("YES").c_str(), restart_es_fx, _("NO").c_str(), nullptr)); }); - } else { + } + else + { row.makeAcceptInputHandler(restart_es_fx); } - row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(window, _("RESTART EMULATIONSTATION"), Font::get(FONT_SIZE_MEDIUM), getMenuTextColor()), true); s->addRow(row); - if(Settings::getInstance()->getBool("ShowExit")) + if (Settings::getInstance()->getBool("ShowExit")) { auto static quit_es_fx = [] { Scripting::fireEvent("quit"); @@ -547,14 +737,17 @@ void GuiMenu::openQuitMenu() }; row.elements.clear(); - if (confirm_quit) { + if (confirm_quit) + { row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY QUIT?", "YES", quit_es_fx, "NO", nullptr)); + window->pushGui(new GuiMsgBox(window, _("REALLY QUIT?"), _("YES").c_str(), quit_es_fx, _("NO").c_str(), nullptr)); }); - } else { + } + else + { row.makeAcceptInputHandler(quit_es_fx); } - row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(window, _("QUIT EMULATIONSTATION"), Font::get(FONT_SIZE_MEDIUM), getMenuTextColor()), true); s->addRow(row); } } @@ -562,62 +755,79 @@ void GuiMenu::openQuitMenu() auto static reboot_sys_fx = [] { Scripting::fireEvent("quit", "reboot"); Scripting::fireEvent("reboot"); - if (quitES(QuitMode::REBOOT)) { + if (quitES(QuitMode::REBOOT)) + { LOG(LogWarning) << "Restart terminated with non-zero result!"; } }; row.elements.clear(); - if (confirm_quit) { + if (confirm_quit) + { row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", {reboot_sys_fx}, "NO", nullptr)); + window->pushGui(new GuiMsgBox(window, _("REALLY RESTART?"), _("YES").c_str(), { reboot_sys_fx }, _("NO").c_str(), nullptr)); }); - } else { + } + else + { row.makeAcceptInputHandler(reboot_sys_fx); } - row.addElement(std::make_shared(window, "RESTART SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(window, _("RESTART SYSTEM"), Font::get(FONT_SIZE_MEDIUM), getMenuTextColor()), true); s->addRow(row); auto static shutdown_sys_fx = [] { Scripting::fireEvent("quit", "shutdown"); Scripting::fireEvent("shutdown"); - if (quitES(QuitMode::SHUTDOWN)) { + if (quitES(QuitMode::SHUTDOWN)) + { LOG(LogWarning) << "Shutdown terminated with non-zero result!"; } }; row.elements.clear(); - if (confirm_quit) { + if (confirm_quit) + { row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY SHUTDOWN?", "YES", shutdown_sys_fx, "NO", nullptr)); + window->pushGui(new GuiMsgBox(window, _("REALLY SHUTDOWN?"), _("YES").c_str(), shutdown_sys_fx, _("NO").c_str(), nullptr)); }); - } else { + } + else + { row.makeAcceptInputHandler(shutdown_sys_fx); } - row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(window, _("SHUTDOWN SYSTEM"), Font::get(FONT_SIZE_MEDIUM), getMenuTextColor()), true); s->addRow(row); + mWindow->pushGui(s); } void GuiMenu::addVersionInfo() { - std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); + std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string(" (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); mVersion.setFont(Font::get(FONT_SIZE_SMALL)); - mVersion.setColor(0x5E5E5EFF); + mVersion.setColor(getVersionTextColor()); mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); mVersion.setHorizontalAlignment(ALIGN_CENTER); addChild(&mVersion); } -void GuiMenu::openScreensaverOptions() { - mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, "SCREENSAVER SETTINGS")); +void GuiMenu::openScreensaverOptions() +{ + mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _("SCREENSAVER SETTINGS").c_str())); } -void GuiMenu::openCollectionSystemSettings() { +void GuiMenu::openCollectionSystemSettings() +{ mWindow->pushGui(new GuiCollectionSystemsOptions(mWindow)); } +// NUEVO: lógica para abrir la GUI de opciones de tema +void GuiMenu::openThemeOptions() +{ + mWindow->pushGui(new GuiThemeOptions(mWindow)); +} + void GuiMenu::onSizeChanged() { mVersion.setSize(mSize.x(), 0); @@ -628,28 +838,29 @@ void GuiMenu::addEntry(const char* name, unsigned int color, bool add_arrow, con { std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); - // populate the list ComponentListRow row; row.addElement(std::make_shared(mWindow, name, font, color), true); - if(add_arrow) + if (add_arrow) { std::shared_ptr 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)) + if (GuiComponent::input(config, input)) return true; - if((config->isMappedTo("b", input) || config->isMappedTo("start", input)) && input.value != 0) + if ((config->isMappedTo("b", input) || config->isMappedTo("start", input)) && input.value != 0) { + // Sonido de BACK vía NavigationSounds ("back") + playMenuBackSound(); + delete this; return true; } @@ -667,8 +878,8 @@ HelpStyle GuiMenu::getHelpStyle() std::vector GuiMenu::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("up/down", "choose")); - prompts.push_back(HelpPrompt("a", "select")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("up/down", _("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _("SELECT"))); + prompts.push_back(HelpPrompt("start", _("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiMenu.h b/es-app/src/guis/GuiMenu.h index f251a97123..82a960f77f 100644 --- a/es-app/src/guis/GuiMenu.h +++ b/es-app/src/guis/GuiMenu.h @@ -2,38 +2,44 @@ #ifndef ES_APP_GUIS_GUI_MENU_H #define ES_APP_GUIS_GUI_MENU_H -#include "components/MenuComponent.h" #include "GuiComponent.h" -#include "components/OptionListComponent.h" -#include "FileData.h" +#include "components/MenuComponent.h" +#include "components/TextComponent.h" +#include "HelpStyle.h" +#include "HelpPrompt.h" class GuiMenu : public GuiComponent { public: GuiMenu(Window* window); + virtual ~GuiMenu() {} bool input(InputConfig* config, Input input) override; - void onSizeChanged() override; std::vector getHelpPrompts() override; HelpStyle getHelpStyle() override; private: - void addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func); - void addVersionInfo(); - void openCollectionSystemSettings(); - void openConfigInput(); - void openOtherSettings(); - void openQuitMenu(); + MenuComponent mMenu; + TextComponent mVersion; + + // Bloques del menú principal void openScraperSettings(); - void openScreensaverOptions(); void openSoundSettings(); void openUISettings(); + void openOtherSettings(); + void openConfigInput(); + void openQuitMenu(); + void openCollectionSystemSettings(); + void openScreensaverOptions(); - MenuComponent mMenu; - TextComponent mVersion; + // NUEVO: opciones de tema internas (GuiThemeOptions) + void openThemeOptions(); + + // Helpers internos + void addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func); + void addVersionInfo(); - typedef OptionListComponent SortList; - std::shared_ptr mListSort; + void onSizeChanged() override; }; #endif // ES_APP_GUIS_GUI_MENU_H diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 28b87db40f..dd3c6f3e53 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -10,6 +10,7 @@ #include "PowerSaver.h" #include "SystemData.h" #include "Window.h" +#include "LocaleESHook.h" // ← traducción (es_translate) GuiScraperMulti::GuiScraperMulti(Window* window, const std::queue& searches, bool approveResults) : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 5)), @@ -28,45 +29,86 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queue(mWindow, "SCRAPING IN PROGRESS", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); + // título + mTitle = std::make_shared( + mWindow, + es_translate("SCRAPING IN PROGRESS"), + Font::get(FONT_SIZE_LARGE), + 0x555555FF, + ALIGN_CENTER); mGrid.setEntry(mTitle, Vector2i(0, 0), false, true); - mSystem = std::make_shared(mWindow, "SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); + // sistema + mSystem = std::make_shared( + mWindow, + es_translate("SYSTEM"), + Font::get(FONT_SIZE_MEDIUM), + 0x777777FF, + ALIGN_CENTER); mGrid.setEntry(mSystem, Vector2i(0, 1), false, true); - mSubtitle = std::make_shared(mWindow, "subtitle text", Font::get(FONT_SIZE_SMALL), 0x888888FF, ALIGN_CENTER); + // subtítulo (se actualiza en doNextSearch) + mSubtitle = std::make_shared( + mWindow, + "", + Font::get(FONT_SIZE_SMALL), + 0x888888FF, + ALIGN_CENTER); mGrid.setEntry(mSubtitle, Vector2i(0, 2), false, true); - mSearchComp = std::make_shared(mWindow, - approveResults ? ScraperSearchComponent::ALWAYS_ACCEPT_MATCHING_CRC : ScraperSearchComponent::ALWAYS_ACCEPT_FIRST_RESULT); + // componente de búsqueda + mSearchComp = std::make_shared( + 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); + mGrid.setEntry( + mSearchComp, + Vector2i(0, 3), + mSearchComp->getSearchType() != ScraperSearchComponent::ALWAYS_ACCEPT_FIRST_RESULT, + true); std::vector< std::shared_ptr > buttons; - if(approveResults) + if (approveResults) { - buttons.push_back(std::make_shared(mWindow, "INPUT", "search", [&] { - mSearchComp->openInputScreen(mSearchQueue.front()); - mGrid.resetCursor(); - })); - - buttons.push_back(std::make_shared(mWindow, "SKIP", "skip", [&] { - skip(); - mGrid.resetCursor(); - })); + // INPUT + buttons.push_back(std::make_shared( + mWindow, + es_translate("INPUT"), + es_translate("SEARCH"), + [&] { + mSearchComp->openInputScreen(mSearchQueue.front()); + mGrid.resetCursor(); + })); + + // SKIP + buttons.push_back(std::make_shared( + mWindow, + es_translate("SKIP"), + es_translate("SKIP"), + [&] { + skip(); + mGrid.resetCursor(); + })); } - buttons.push_back(std::make_shared(mWindow, "STOP", "stop (progress saved)", std::bind(&GuiScraperMulti::finish, this))); + // STOP + buttons.push_back(std::make_shared( + mWindow, + es_translate("STOP"), + es_translate("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); + setPosition( + (Renderer::getScreenWidth() - mSize.x()) / 2, + (Renderer::getScreenHeight() - mSize.y()) / 2); doNextSearch(); } @@ -74,7 +116,7 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queue detailed) - for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) ViewController::get()->reloadGameListView(*it, false); } @@ -91,19 +133,25 @@ void GuiScraperMulti::onSizeChanged() void GuiScraperMulti::doNextSearch() { - if(mSearchQueue.empty()) + if (mSearchQueue.empty()) { finish(); return; } - // update title - std::stringstream ss; + // sistema actual mSystem->setText(Utils::String::toUpper(mSearchQueue.front().system->getFullName())); - // update subtitle - ss.str(""); // clear - ss << "GAME " << (mCurrentGame + 1) << " OF " << mTotalGames << " - " << Utils::String::toUpper(Utils::FileSystem::getFileName(mSearchQueue.front().game->getPath())); + // subtítulo: "GAME X OF Y - NOMBRE.EXT" + std::stringstream ss; + std::string gameLabel = es_translate("GAME"); + std::string ofLabel = es_translate("OF"); + + ss << gameLabel << " " << (mCurrentGame + 1) + << " " << ofLabel << " " << mTotalGames + << " - " + << Utils::String::toUpper(Utils::FileSystem::getFileName(mSearchQueue.front().game->getPath())); + mSubtitle->setText(ss.str()); mSearchComp->search(mSearchQueue.front()); @@ -133,18 +181,35 @@ void GuiScraperMulti::skip() 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."; + if (mTotalSuccessful == 0) + { + ss << es_translate("NO GAMES WERE SCRAPED."); + } + else + { + // X GAME(S) SUCCESSFULLY SCRAPED! + if (mTotalSuccessful == 1) + ss << mTotalSuccessful << " " << es_translate("GAME SCRAPED"); + else + ss << mTotalSuccessful << " " << es_translate("GAMES SCRAPED"); + + // Y GAME(S) SKIPPED. + if (mTotalSkipped > 0) + { + ss << "\n"; + if (mTotalSkipped == 1) + ss << mTotalSkipped << " " << es_translate("GAME SKIPPED"); + else + ss << mTotalSkipped << " " << es_translate("GAMES SKIPPED"); + } } - mWindow->pushGui(new GuiMsgBox(mWindow, ss.str(), - "OK", [&] { delete this; })); + mWindow->pushGui(new GuiMsgBox( + mWindow, + ss.str(), + es_translate("OK"), + [&] { delete this; })); mIsProcessing = false; PowerSaver::resume(); diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index 2242046901..84b7dd155a 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -7,122 +7,125 @@ #include "views/ViewController.h" #include "FileData.h" #include "SystemData.h" +#include "LocaleESHook.h" // 🔹 Traducción disponible (ruta correcta) GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), - mMenu(window, "SCRAPE NOW") + mMenu(window, es_translate("SCRAPE NOW").c_str()) { - addChild(&mMenu); - - // add filters (with first one selected) - mFilters = std::make_shared< OptionListComponent >(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 >(mWindow, "SCRAPE THESE SYSTEMS", true); - for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) - { - if(!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) - mSystems->add((*it)->getFullName(), *it, !(*it)->getPlatformIds().empty()); - } - mMenu.addWithLabel("Systems", mSystems); - - mApproveResults = std::make_shared(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); + addChild(&mMenu); + + // Filters + mFilters = std::make_shared< OptionListComponent >(mWindow, es_translate("SCRAPE THESE GAMES"), false); + mFilters->add(es_translate("ALL GAMES"), + [](SystemData*, FileData*) -> bool { return true; }, false); + mFilters->add(es_translate("ONLY MISSING IMAGE"), + [](SystemData*, FileData* g) -> bool { return g->metadata.get("image").empty(); }, true); + mMenu.addWithLabel(es_translate("FILTER"), mFilters); + + // Systems + mSystems = std::make_shared< OptionListComponent >(mWindow, es_translate("SCRAPE THESE SYSTEMS"), true); + for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + { + if(!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) + mSystems->add((*it)->getFullName(), *it, !(*it)->getPlatformIds().empty()); + } + mMenu.addWithLabel(es_translate("SYSTEMS"), mSystems); + + // Conflicts + mApproveResults = std::make_shared(mWindow); + mApproveResults->setState(true); + mMenu.addWithLabel(es_translate("USER DECIDES ON CONFLICTS"), mApproveResults); + + // Buttons + mMenu.addButton(es_translate("START"), es_translate("START"), std::bind(&GuiScraperStart::pressedStart, this)); + mMenu.addButton(es_translate("BACK"), es_translate("BACK"), [&] { delete this; }); + + mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, + Renderer::getScreenHeight() * 0.15f); } void GuiScraperStart::pressedStart() { - std::vector sys = mSystems->getSelectedObjects(); - for(auto it = sys.cbegin(); it != sys.cend(); it++) - { - if((*it)->getPlatformIds().empty()) - { - mWindow->pushGui(new GuiMsgBox(mWindow, - Utils::String::toUpper("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(); + std::vector sys = mSystems->getSelectedObjects(); + for(auto it = sys.cbegin(); it != sys.cend(); it++) + { + if((*it)->getPlatformIds().empty()) + { + mWindow->pushGui(new GuiMsgBox(mWindow, + es_translate("WARNING: SOME SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE INACCURATE.\nCONTINUE ANYWAY?"), + es_translate("YES"), std::bind(&GuiScraperStart::start, this), + es_translate("NO"), nullptr)); + return; + } + } + + start(); } void GuiScraperStart::start() { - std::queue 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 searches = getSearches(mSystems->getSelectedObjects(), mFilters->getSelected()); + + if(searches.empty()) + { + mWindow->pushGui(new GuiMsgBox(mWindow, es_translate("NO GAMES FIT THAT CRITERIA."))); + } + else + { + GuiScraperMulti* gsm = new GuiScraperMulti(mWindow, searches, mApproveResults->getState()); + mWindow->pushGui(gsm); + delete this; + } } std::queue GuiScraperStart::getSearches(std::vector systems, GameFilterFunc selector) { - std::queue queue; - for(auto sys = systems.cbegin(); sys != systems.cend(); sys++) - { - std::vector games = (*sys)->getRootFolder()->getFilesRecursive(GAME); - for(auto game = games.cbegin(); game != games.cend(); game++) - { - if(selector((*sys), (*game))) - { - ScraperSearchParams search; - search.game = *game; - search.system = *sys; - - queue.push(search); - } - } - } - - return queue; + std::queue queue; + for(auto sys = systems.cbegin(); sys != systems.cend(); sys++) + { + std::vector games = (*sys)->getRootFolder()->getFilesRecursive(GAME); + for(auto game = games.cbegin(); game != games.cend(); 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; + 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 GuiScraperStart::getHelpPrompts() { - std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); - return prompts; + std::vector prompts = mMenu.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", es_translate("BACK"))); + prompts.push_back(HelpPrompt("start", es_translate("CLOSE"))); + return prompts; } diff --git a/es-app/src/guis/GuiThemeOptions.cpp b/es-app/src/guis/GuiThemeOptions.cpp new file mode 100644 index 0000000000..600e3749ea --- /dev/null +++ b/es-app/src/guis/GuiThemeOptions.cpp @@ -0,0 +1,790 @@ +// es-app/src/guis/GuiThemeOptions.cpp + +#include "guis/GuiThemeOptions.h" +#include "guis/GuiMsgBox.h" +#include "guis/GuiTextEditPopup.h" + +#include "LocaleES.h" +#include "Settings.h" +#include "utils/FileSystemUtil.h" +#include "resources/Font.h" +#include "views/ViewController.h" +#include "SystemData.h" +#include "Window.h" +#include "renderers/Renderer.h" + +#include +#include +#include +#include + +namespace +{ + inline std::string _(const std::string& key) + { + return LocaleES::getInstance().translate(key); + } + + struct ThemeOption + { + std::string id; + std::string type; + std::string label; + std::string path; + std::string applyTo; + + // Optional output key for theme.ini (e.g. key=USER_NAME) + std::string key; + + std::vector> values; + std::string defaultValue; + }; + + // ----------------------------- + // Small helpers (ES-X compatible) + // ----------------------------- + static std::string trim(const std::string& s) + { + size_t start = s.find_first_not_of(" \t\r\n"); + if(start == std::string::npos) + return ""; + size_t end = s.find_last_not_of(" \t\r\n"); + return s.substr(start, end - start + 1); + } + + static bool isAbsolutePathSimple(const std::string& p) + { + // Linux/Unix absolute + return !p.empty() && p[0] == '/'; + } + + static std::string joinPathSimple(const std::string& a, const std::string& b) + { + if(a.empty()) return b; + if(b.empty()) return a; + + if(a.back() == '/' && b.front() == '/') + return a + b.substr(1); + if(a.back() != '/' && b.front() != '/') + return a + "/" + b; + return a + b; + } + + static std::string normalizeRelative(const std::string& p) + { + // remove leading "./" + if(p.rfind("./", 0) == 0) + return p.substr(2); + return p; + } + + static std::string resolveThemePath(const std::string& themeDir, const std::string& maybeRelative) + { + if(maybeRelative.empty()) + return ""; + + if(isAbsolutePathSimple(maybeRelative)) + return maybeRelative; + + return joinPathSimple(themeDir, normalizeRelative(maybeRelative)); + } + + static bool copyFileForce(const std::string& src, const std::string& dst) + { + std::ifstream in(src.c_str(), std::ios::binary); + if(!in) + return false; + + std::ofstream out(dst.c_str(), std::ios::binary | std::ios::trunc); + if(!out) + return false; + + out << in.rdbuf(); + return out.good(); + } + + static void parseValues(const std::string& str, std::vector>& out) + { + out.clear(); + std::stringstream ss(str); + std::string item; + + while(std::getline(ss, item, ',')) + { + item = trim(item); + if(item.empty()) + continue; + + auto pos = item.find('|'); + if(pos == std::string::npos) + out.emplace_back(item, item); + else + { + std::string internal = trim(item.substr(0, pos)); + std::string label = trim(item.substr(pos + 1)); + if(!internal.empty() && !label.empty()) + out.emplace_back(internal, label); + } + } + } + + // ------------------------------------------------------------ + // Read INI sections into a map: section -> (key->value) + // ------------------------------------------------------------ + static std::map> + loadIniSections(const std::string& iniPath) + { + std::map> data; + + std::ifstream file(iniPath.c_str()); + if(!file) + return data; + + std::string line; + std::string currentSection; + + while(std::getline(file, line)) + { + line = trim(line); + if(line.empty() || line[0] == ';' || line[0] == '#') + continue; + + if(line.front() == '[' && line.back() == ']') + { + currentSection = trim(line.substr(1, line.size() - 2)); + continue; + } + + auto pos = line.find('='); + if(pos == std::string::npos) + continue; + + std::string key = trim(line.substr(0, pos)); + std::string val = trim(line.substr(pos + 1)); + + if(!currentSection.empty() && !key.empty()) + data[currentSection][key] = val; + } + + return data; + } + + // ------------------------------------------------------------ + // Read top-level key=value (before first [section]) + // ------------------------------------------------------------ + static std::string readTopIniValue(const std::string& iniPath, const std::string& key) + { + std::ifstream in(iniPath.c_str()); + if(!in) + return ""; + + std::string line; + bool inTop = true; + + while(std::getline(in, line)) + { + std::string t = trim(line); + + if(inTop && !t.empty() && t.front() == '[') + break; + + if(t.empty() || t[0] == ';' || t[0] == '#') + continue; + + auto posEq = t.find('='); + if(posEq == std::string::npos) + continue; + + std::string thisKey = trim(t.substr(0, posEq)); + if(thisKey == key) + return trim(t.substr(posEq + 1)); + } + + return ""; + } + + static void updateThemeIniValue(const std::string& iniPath, const std::string& key, const std::string& value) + { + std::ifstream in(iniPath.c_str()); + if(!in) + return; + + std::vector lines; + std::string line; + bool updated = false; + bool inTop = true; + + while(std::getline(in, line)) + { + std::string t = trim(line); + + if(inTop && !t.empty() && t.front() == '[') + inTop = false; + + if(inTop) + { + auto posEq = t.find('='); + if(posEq != std::string::npos) + { + std::string thisKey = trim(t.substr(0, posEq)); + if(thisKey == key) + { + line = key + " = " + value; + updated = true; + } + } + } + lines.push_back(line); + } + in.close(); + + if(!updated) + { + size_t insertPos = lines.size(); + for(size_t i = 0; i < lines.size(); ++i) + { + std::string t = trim(lines[i]); + if(!t.empty() && t.front() == '[') + { + insertPos = i; + break; + } + } + lines.insert(lines.begin() + insertPos, key + " = " + value); + } + + std::ofstream out(iniPath.c_str(), std::ios::trunc); + if(!out) + return; + for(const auto& l : lines) + out << l << "\n"; + } + + // ------------------------------------------------------------ + // Auto-detect /layout subfolders and add [layout] option if missing + // ------------------------------------------------------------ + static void ensureLayoutOptionIfLayoutFolderExists(const std::string& themeDir, std::vector& options) + { + const std::string layoutDir = themeDir + "/layout"; + if(!Utils::FileSystem::isDirectory(layoutDir)) + return; + + for(const auto& o : options) + if(o.id == "layout") + return; + + Utils::FileSystem::stringList lst = Utils::FileSystem::getDirContent(layoutDir); + std::vector dirs(lst.begin(), lst.end()); + + std::vector layouts; + for(const auto& name : dirs) + { + if(name.empty() || name[0] == '.') + continue; + + const std::string full = layoutDir + "/" + name; + if(Utils::FileSystem::isDirectory(full)) + layouts.push_back(name); + } + + if(layouts.empty()) + return; + + std::sort(layouts.begin(), layouts.end()); + + ThemeOption layoutOpt; + layoutOpt.id = "layout"; + layoutOpt.type = "select"; + layoutOpt.applyTo = "layout"; + layoutOpt.label = _("THEME LAYOUT"); + + for(const auto& l : layouts) + layoutOpt.values.emplace_back(l, l); + + const std::string current = Settings::getInstance()->getString("ThemeLayout"); + layoutOpt.defaultValue = !current.empty() ? current : layouts.front(); + + options.insert(options.begin(), layoutOpt); + } + + // ------------------------------------------------------------ + // Auto-detect /_inc/avatars images and add [avatar] option if missing + // ------------------------------------------------------------ + static bool hasImageExt(const std::string& f) + { + auto lower = f; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + + return (lower.size() >= 4 && lower.rfind(".png") == lower.size() - 4) || + (lower.size() >= 4 && lower.rfind(".jpg") == lower.size() - 4) || + (lower.size() >= 5 && lower.rfind(".jpeg") == lower.size() - 5); + } + + static std::string stripExt(const std::string& f) + { + auto pos = f.find_last_of('.'); + if(pos == std::string::npos) return f; + return f.substr(0, pos); + } + + static void ensureAvatarOptionIfAvatarFolderExists(const std::string& themeDir, std::vector& options) + { + // If theme.ini already declared [avatar], do nothing. + for(const auto& o : options) + if(o.id == "avatar") + return; + + const std::string avatarDir = themeDir + "/_inc/avatars"; + if(!Utils::FileSystem::isDirectory(avatarDir)) + return; + + Utils::FileSystem::stringList lst = Utils::FileSystem::getDirContent(avatarDir); + std::vector files(lst.begin(), lst.end()); + + std::vector imgs; + for(const auto& name : files) + { + if(name.empty() || name[0] == '.') + continue; + + const std::string full = avatarDir + "/" + name; + if(Utils::FileSystem::isRegularFile(full) && hasImageExt(name)) + imgs.push_back(name); + } + + if(imgs.empty()) + return; + + std::sort(imgs.begin(), imgs.end()); + + ThemeOption opt; + opt.id = "avatar"; + opt.type = "select"; + opt.applyTo = "theme_ini"; + opt.key = "AVATAR_PATH"; + opt.label = _("AVATAR"); + + for(const auto& f : imgs) + { + const std::string relPath = "./_inc/avatars/" + f; + const std::string nice = stripExt(f); + opt.values.emplace_back(relPath, nice); + } + + const std::string iniPath = themeDir + "/theme.ini"; + std::string current = readTopIniValue(iniPath, opt.key); + if(!current.empty()) + opt.defaultValue = current; + else + opt.defaultValue = opt.values.front().first; + + options.push_back(opt); + } + + // ------------------------------------------------------------ + // Load theme.ini options + // ------------------------------------------------------------ + static std::vector loadThemeOptionsFromIni(const std::string& themeDir) + { + std::vector options; + const std::string iniPath = themeDir + "/theme.ini"; + + if(Utils::FileSystem::isRegularFile(iniPath)) + { + std::ifstream file(iniPath.c_str()); + if(file) + { + ThemeOption current; + std::string line; + + auto pushIfValid = [&options](const ThemeOption& opt) + { + if(opt.id.empty() || opt.type.empty()) + return; + + std::string t = opt.type; + std::transform(t.begin(), t.end(), t.begin(), ::tolower); + + // select requires values, input does not + if(t == "select") + { + if(!opt.values.empty()) + options.push_back(opt); + } + else if(t == "input") + { + options.push_back(opt); + } + }; + + while(std::getline(file, line)) + { + line = trim(line); + if(line.empty() || line[0] == ';' || line[0] == '#') + continue; + + if(line.front() == '[' && line.back() == ']') + { + pushIfValid(current); + current = ThemeOption(); + current.id = trim(line.substr(1, line.size() - 2)); + continue; + } + + auto pos = line.find('='); + if(pos != std::string::npos) + { + std::string key = trim(line.substr(0, pos)); + std::string value = trim(line.substr(pos + 1)); + + if(key == "label") current.label = value; + else if(key == "type") current.type = value; + else if(key == "path") current.path = value; + else if(key == "apply_to" || key == "applyTo") current.applyTo = value; + else if(key == "values") parseValues(value, current.values); + else if(key == "default") current.defaultValue = value; + else if(key == "key") current.key = value; + } + } + + pushIfValid(current); + } + } + + ensureLayoutOptionIfLayoutFolderExists(themeDir, options); + ensureAvatarOptionIfAvatarFolderExists(themeDir, options); + return options; + } + + // ------------------------------------------------------------ + // Apply layout by COPYING layout files to theme root (dialog-style) + // ------------------------------------------------------------ + static bool applyLayoutByCopy(Window* win, const std::string& themeDir, const std::string& layoutId) + { + const std::string iniPath = themeDir + "/theme.ini"; + std::map> ini; + if(Utils::FileSystem::isRegularFile(iniPath)) + ini = loadIniSections(iniPath); + + // Defaults: ./layout//.xml + auto defPath = [&](const std::string& fileName) -> std::string + { + return "./layout/" + layoutId + "/" + fileName; + }; + + const std::string section = "layout_" + layoutId; + + std::string srcTheme = defPath("theme.xml"); + std::string srcSwatch = defPath("Swatch.xml"); + std::string srcAvatar = defPath("avatar.xml"); + std::string srcUser = defPath("user.xml"); + std::string srcStart = defPath("start.xml"); + + // If mapping exists in ini, override + if(!ini.empty() && ini.count(section)) + { + auto& s = ini[section]; + if(s.count("theme")) srcTheme = s["theme"]; + if(s.count("swatch")) srcSwatch = s["swatch"]; + if(s.count("avatar")) srcAvatar = s["avatar"]; + if(s.count("user")) srcUser = s["user"]; + if(s.count("start")) srcStart = s["start"]; + } + + // Resolve paths + const std::string absTheme = resolveThemePath(themeDir, srcTheme); + const std::string absSwatch = resolveThemePath(themeDir, srcSwatch); + const std::string absAvatar = resolveThemePath(themeDir, srcAvatar); + const std::string absUser = resolveThemePath(themeDir, srcUser); + const std::string absStart = resolveThemePath(themeDir, srcStart); + + bool okAny = false; + bool okAll = true; + + auto doCopy = [&](const std::string& srcAbs, const std::string& dstName) + { + if(srcAbs.empty()) + return; + + if(!Utils::FileSystem::isRegularFile(srcAbs)) + { + // Not fatal; just skip + return; + } + + const std::string dstAbs = themeDir + "/" + dstName; + if(!copyFileForce(srcAbs, dstAbs)) + okAll = false; + else + okAny = true; + }; + + doCopy(absTheme, "theme.xml"); + doCopy(absSwatch, "Swatch.xml"); + doCopy(absAvatar, "avatar.xml"); + doCopy(absUser, "user.xml"); + doCopy(absStart, "start.xml"); + + if(!okAny) + { + if(win) + win->pushGui(new GuiMsgBox(win, _("No se encontró ningún XML para copiar en este layout."), _("BACK"))); + return false; + } + + if(!okAll) + { + if(win) + win->pushGui(new GuiMsgBox(win, _("Algunos archivos no se pudieron copiar (revisá permisos/rutas)."), _("BACK"))); + } + + return true; + } + + static void applyThemeOption(Window* win, const std::string& themeDir, const ThemeOption& opt, const std::string& value) + { + if(opt.id.empty() || value.empty()) + return; + + if(opt.applyTo == "layout" || opt.id == "layout") + { + Settings::getInstance()->setString("ThemeLayout", value); + Settings::getInstance()->saveFile(); + + // IMPORTANT: copy layout files to theme root (dialog behavior) + applyLayoutByCopy(win, themeDir, value); + } + else + { + const std::string iniPath = themeDir + "/theme.ini"; + const std::string outKey = !opt.key.empty() ? opt.key : opt.id; + updateThemeIniValue(iniPath, outKey, value); + } + + auto vc = ViewController::get(); + if(vc != nullptr) + vc->reloadAll(); + } + + class GuiThemeOptionSelect : public GuiComponent + { + public: + GuiThemeOptionSelect(Window* window, + const std::string& themeDir, + const ThemeOption& opt, + const std::string& title) + : GuiComponent(window) + , mMenu(window, title.c_str()) + , mThemeDir(themeDir) + , mOption(opt) + { + for(const auto& v : mOption.values) + { + ComponentListRow row; + + auto text = std::make_shared( + mWindow, + v.second, + Font::get(FONT_SIZE_MEDIUM), + 0x777777FF + ); + + text->setColor(0x777777FF); + row.addElement(text, true); + + row.makeAcceptInputHandler([this, v]() + { + applyThemeOption(mWindow, mThemeDir, mOption, v.first); + delete this; + }); + + mMenu.addRow(row); + } + + addChild(&mMenu); + + setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + mMenu.setPosition( + (mSize.x() - mMenu.getSize().x()) / 2.0f, + Renderer::getScreenHeight() * 0.15f); + } + + bool input(InputConfig* config, Input input) override + { + if(GuiComponent::input(config, input)) + return true; + + if(config->isMappedTo("b", input) && input.value != 0) + { + delete this; + return true; + } + return false; + } + + std::vector getHelpPrompts() override + { + std::vector prompts; + prompts.push_back(HelpPrompt("a", _("SELECT"))); + prompts.push_back(HelpPrompt("b", _("BACK"))); + return prompts; + } + + HelpStyle getHelpStyle() override + { + HelpStyle style; + auto vc = ViewController::get(); + if(vc) + { + auto system = vc->getState().getSystem(); + if(system) + style.applyTheme(system->getTheme(), "system"); + } + return style; + } + + private: + MenuComponent mMenu; + std::string mThemeDir; + ThemeOption mOption; + }; + + static void openThemeInput(Window* win, const std::string& themeDir, const ThemeOption& opt, const std::string& title) + { + if(win == nullptr) + return; + + const std::string iniPath = themeDir + "/theme.ini"; + const std::string outKey = !opt.key.empty() ? opt.key : opt.id; + + std::string current = readTopIniValue(iniPath, outKey); + if(current.empty()) + current = opt.defaultValue; + + // NOTE: acceptBtnText is const char* and may be stored as pointer. + // Use static std::string to guarantee lifetime and allow translation. + static std::string okLabel = _("OK"); + + win->pushGui(new GuiTextEditPopup( + win, + title, + current, + [win, themeDir, opt](const std::string& newValue) + { + if(newValue.empty()) + return; + + applyThemeOption(win, themeDir, opt, newValue); + }, + false, // multiline + okLabel.c_str() // safe lifetime + )); + } +} + +GuiThemeOptions::GuiThemeOptions(Window* window) + : GuiComponent(window) + , mMenu(window, _("THEME OPTIONS").c_str()) +{ + std::string themeSet = Settings::getInstance()->getString("ThemeSet"); + std::string themeDir; + + if(!themeSet.empty()) + themeDir = Utils::FileSystem::getHomePath() + "/.emulationstation/themes/" + themeSet; + + auto options = loadThemeOptionsFromIni(themeDir); + + if(options.empty()) + { + ComponentListRow row; + row.addElement( + std::make_shared( + mWindow, + _("NO THEME OPTIONS AVAILABLE"), + Font::get(FONT_SIZE_MEDIUM), + 0x777777FF), + true); + mMenu.addRow(row); + } + else + { + for(const auto& opt : options) + { + std::string entryLabel = !opt.label.empty() ? opt.label : opt.id; + + ComponentListRow row; + + auto text = std::make_shared( + mWindow, + entryLabel, + Font::get(FONT_SIZE_MEDIUM), + 0x777777FF + ); + text->setColor(0x777777FF); + + row.addElement(text, true); + + row.makeAcceptInputHandler([this, themeDir, opt, entryLabel] + { + std::string t = opt.type; + std::transform(t.begin(), t.end(), t.begin(), ::tolower); + + if(t == "select" && !opt.values.empty()) + { + mWindow->pushGui(new GuiThemeOptionSelect(mWindow, themeDir, opt, entryLabel)); + } + else if(t == "input") + { + openThemeInput(mWindow, themeDir, opt, entryLabel); + } + else + { + std::string msg = entryLabel + "\n\n" + _("(Feature not yet implemented)"); + mWindow->pushGui(new GuiMsgBox(mWindow, msg, _("BACK"))); + } + }); + + mMenu.addRow(row); + } + } + + addChild(&mMenu); + + setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + mMenu.setPosition( + (mSize.x() - mMenu.getSize().x()) / 2.0f, + Renderer::getScreenHeight() * 0.15f); +} + +bool GuiThemeOptions::input(InputConfig* config, Input input) +{ + if(GuiComponent::input(config, input)) + return true; + + if(config->isMappedTo("b", input) && input.value != 0) + { + delete this; + return true; + } + return false; +} + +std::vector GuiThemeOptions::getHelpPrompts() +{ + std::vector prompts; + prompts.push_back(HelpPrompt("b", _("BACK"))); + return prompts; +} + +HelpStyle GuiThemeOptions::getHelpStyle() +{ + HelpStyle style; + auto vc = ViewController::get(); + if(vc) + { + auto system = vc->getState().getSystem(); + if(system) + style.applyTheme(system->getTheme(), "system"); + } + return style; +} diff --git a/es-app/src/guis/GuiThemeOptions.h b/es-app/src/guis/GuiThemeOptions.h new file mode 100644 index 0000000000..d366c0c0ed --- /dev/null +++ b/es-app/src/guis/GuiThemeOptions.h @@ -0,0 +1,18 @@ +// es-app/src/guis/GuiThemeOptions.h +#pragma once + +#include "GuiComponent.h" +#include "components/MenuComponent.h" + +class GuiThemeOptions : public GuiComponent +{ +public: + explicit GuiThemeOptions(Window* window); + + bool input(InputConfig* config, Input input) override; + std::vector getHelpPrompts() override; + HelpStyle getHelpStyle() override; + +private: + MenuComponent mMenu; +}; diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index eb0b92cacf..fa3e963e2f 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -28,6 +28,12 @@ #include +// NUEVO: sistema de localización ES-X +#include "LocaleES.h" + +// NUEVO: música de fondo ES-X +#include "audio/BackgroundMusicManager.h" + bool scrape_cmdline = false; bool parseArgs(int argc, char* argv[]) @@ -173,10 +179,6 @@ bool parseArgs(int argc, char* argv[]) else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { #ifdef WIN32 - // This is a bit of a hack, but otherwise output will go to nowhere - // when the application is compiled with the "WINDOWS" subsystem (which we usually are). - // If you're an experienced Windows programmer and know how to do this - // the right way, please submit a pull request! AttachConsole(ATTACH_PARENT_PROCESS); freopen("CONOUT$", "wb", stdout); #endif @@ -231,7 +233,7 @@ bool parseArgs(int argc, char* argv[]) "setting is changed via EmulationStation UI.\n\n" "Please refer to the online documentation for additional information:\n" "https://retropie.org.uk/docs/EmulationStation/\n"; - return false; //exit after printing help + return false; } } @@ -240,7 +242,6 @@ bool parseArgs(int argc, char* argv[]) bool verifyHomeFolderExists() { - //make sure the config directory exists std::string home = Utils::FileSystem::getHomePath(); std::string configDir = home + "/.emulationstation"; if(!Utils::FileSystem::exists(configDir)) @@ -257,7 +258,6 @@ bool verifyHomeFolderExists() return true; } -// Returns true if everything is OK, bool loadSystemConfigFile(Window* window, const char** errorString) { *errorString = NULL; @@ -283,7 +283,6 @@ bool loadSystemConfigFile(Window* window, const char** errorString) return true; } -//called on exit, assuming we get far enough to have the log initialized void onExit() { Log::close(); @@ -296,54 +295,35 @@ int main(int argc, char* argv[]) if(!parseArgs(argc, argv)) return 0; - // only show the console on Windows if HideConsole is false #ifdef WIN32 - // MSVC has a "SubSystem" option, with two primary options: "WINDOWS" and "CONSOLE". - // In "WINDOWS" mode, no console is automatically created for us. This is good, - // because we can choose to only create the console window if the user explicitly - // asks for it, preventing it from flashing open and then closing. - // In "CONSOLE" mode, a console is always automatically created for us before we - // enter main. In this case, we can only hide the console after the fact, which - // will leave a brief flash. - // TL;DR: You should compile ES under the "WINDOWS" subsystem. - // I have no idea how this works with non-MSVC compilers. if(!Settings::getInstance()->getBool("HideConsole")) { - // we want to show the console - // if we're compiled in "CONSOLE" mode, this is already done. - // if we're compiled in "WINDOWS" mode, no console is created for us automatically; - // the user asked for one, so make one and then hook stdin/stdout/sterr up to it - if(AllocConsole()) // should only pass in "WINDOWS" mode + if(AllocConsole()) { freopen("CONIN$", "r", stdin); freopen("CONOUT$", "wb", stdout); freopen("CONOUT$", "wb", stderr); } }else{ - // we want to hide the console - // if we're compiled with the "WINDOWS" subsystem, this is already done. - // if we're compiled with the "CONSOLE" subsystem, a console is already created; - // it'll flash open, but we hide it nearly immediately - if(GetConsoleWindow()) // should only pass in "CONSOLE" mode + if(GetConsoleWindow()) ShowWindow(GetConsoleWindow(), SW_HIDE); } #endif - // call this ONLY when linking with FreeImage as a static library #ifdef FREEIMAGE_LIB FreeImage_Initialise(); #endif - //if ~/.emulationstation doesn't exist and cannot be created, bail if(!verifyHomeFolderExists()) return 1; - //start the logger Log::init(); Log::open(); LOG(LogInfo) << "EmulationStation - v" << PROGRAM_VERSION_STRING << ", built " << PROGRAM_BUILT_STRING; - //always close the log on exit + // 🔤 NUEVO: inicializar localización según Settings::Language + LocaleES::getInstance().loadFromSettings(); + atexit(&onExit); Window window; @@ -374,7 +354,6 @@ int main(int argc, char* argv[]) const char* errorMsg = NULL; if(!loadSystemConfigFile(splashScreen ? &window : nullptr, &errorMsg)) { - // something went terribly wrong if(errorMsg == NULL) { LOG(LogError) << "Unknown error occured while parsing system config file."; @@ -383,7 +362,6 @@ int main(int argc, char* argv[]) 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", [] { @@ -393,14 +371,11 @@ int main(int argc, char* argv[]) })); } - //run the command line scraper then quit if(scrape_cmdline) { return run_scraper_cmdline(); } - // 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(); if(splashScreen) @@ -408,7 +383,9 @@ int main(int argc, char* argv[]) InputManager::getInstance()->init(); - //choose which GUI to open depending on if an input configuration already exists + // 🔊 NUEVO: inicializar música de fondo ES-X (si está habilitada) + BackgroundMusicManager::getInstance().init(); + if(errorMsg == NULL) { if(Utils::FileSystem::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0) @@ -439,25 +416,20 @@ int main(int argc, char* argv[]) running = false; } while(SDL_PollEvent(&event)); - // triggered if exiting from SDL_WaitEvent due to event if (ps_standby) - // show as if continuing from last event lastTime = SDL_GetTicks(); - // reset counter ps_time = SDL_GetTicks(); } else if (ps_standby) { - // If exitting SDL_WaitEventTimeout due to timeout. Trail considering - // timeout as an event ps_time = SDL_GetTicks(); } 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 + SDL_Delay(1); continue; } @@ -465,11 +437,14 @@ int main(int argc, char* argv[]) int deltaTime = curTime - lastTime; lastTime = curTime; - // cap deltaTime if it ever goes negative if(deltaTime < 0) deltaTime = 1000; window.update(deltaTime); + + // ✅ BGM: procesar “resume con delay” al volver del juego + BackgroundMusicManager::getInstance().update(deltaTime); + window.render(); Renderer::swapBuffers(); @@ -480,13 +455,16 @@ int main(int argc, char* argv[]) delete window.peekGui(); InputManager::getInstance()->deinit(); + + // 🔊 NUEVO: apagar música antes de cerrar ventana / audio + BackgroundMusicManager::getInstance().shutdown(); + window.deinit(); MameNames::deinit(); CollectionSystemManager::deinit(); SystemData::deleteSystems(); - // call this ONLY when linking with FreeImage as a static library #ifdef FREEIMAGE_LIB FreeImage_DeInitialise(); #endif diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index da6fd1e0f4..2a7d7c7788 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -7,6 +7,7 @@ #include "PlatformId.h" #include "Settings.h" #include "SystemData.h" +#include "LocaleESHook.h" // Puedes dejarlo aunque aquí ya no lo usamos directamente #include #include @@ -214,26 +215,51 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve ScraperSearchResult result; ScreenScraperRequest::ScreenScraperConfig ssConfig; - std::string region = Utils::String::toLower(ssConfig.region).c_str(); - std::string language = Utils::String::toLower(ssConfig.language).c_str(); - - // Name fallback: US, WOR(LD). ( Xpath: Data/jeu[0]/noms/nom[*] ). - result.mdl.set("name", find_child_by_attribute_list(game.child("noms"), "nom", "region", { region, "wor", "us" , "ss", "eu", "jp" }).text().get()); - - // Description fallback language: EN, WOR(LD) - std::string description = find_child_by_attribute_list(game.child("synopsis"), "synopsis", "langue", { language, "en", "wor" }).text().get(); + // 🔹 Idioma dinámico según el ajuste "Language" (que ya viene del .ini) + std::string language = Settings::getInstance()->getString("Language"); + if (language.empty()) + language = "en"; + language = Utils::String::toLower(language); + + // usamos el idioma como región base; ScreenScraper también usa "wor", "us", etc. + std::string region = language; + + // Name fallback: región, luego WOR(LD), US, EU, JP, SS. + result.mdl.set("name", find_child_by_attribute_list( + game.child("noms"), + "nom", + "region", + { region, "wor", "us" , "eu", "jp", "ss" } + ).text().get()); + + // Description fallback language: idioma actual, EN, WOR(LD) + std::string description = find_child_by_attribute_list( + game.child("synopsis"), + "synopsis", + "langue", + { language, "en", "wor" } + ).text().get(); if (!description.empty()) { result.mdl.set("desc", Utils::String::replace(description, " ", " ")); } - // Genre fallback language: EN. ( Xpath: Data/jeu[0]/genres/genre[*] ) - result.mdl.set("genre", find_child_by_attribute_list(game.child("genres"), "genre", "langue", { language, "en" }).text().get()); + // Genre fallback language: idioma actual, luego EN. + result.mdl.set("genre", find_child_by_attribute_list( + game.child("genres"), + "genre", + "langue", + { language, "en" } + ).text().get()); LOG(LogDebug) << "Genre: " << result.mdl.get("genre"); - // Get the date proper. The API returns multiple 'date' children nodes to the 'dates' main child of 'jeu'. - // Date fallback: WOR(LD), US, SS, JP, EU - std::string _date = find_child_by_attribute_list(game.child("dates"), "date", "region", { region, "wor", "us", "ss", "jp", "eu" }).text().get(); + // Date fallback: región, WOR(LD), US, SS, JP, EU + std::string _date = find_child_by_attribute_list( + game.child("dates"), + "date", + "region", + { region, "wor", "us", "ss", "jp", "eu" } + ).text().get(); LOG(LogDebug) << "Release Date (unparsed): " << _date; // Date can be YYYY-MM-DD or just YYYY. @@ -277,14 +303,13 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve pugi::xml_node art = pugi::xml_node(NULL); // Do an XPath query for media[type='$media_type'], then filter by region - // We need to do this because any child of 'medias' has the form - // - // and we need to find the right media for the region. - pugi::xpath_node_set results = media_list.select_nodes((static_cast("media[@type='") + ssConfig.media_name + "']").c_str()); + pugi::xpath_node_set results = media_list.select_nodes( + (static_cast("media[@type='") + ssConfig.media_name + "']").c_str() + ); if (results.size()) { - // Region fallback: WOR(LD), US, CUS(TOM?), JP, EU + // Region fallback: región actual, WOR(LD), US, CUS(TOM?), JP, EU for (auto _region : std::vector{ region, "wor", "us", "cus", "jp", "eu" }) { if (art) @@ -303,16 +328,15 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve if (art) { - // Sending a 'softname' containing space will make the image URLs returned by the API also contain the space. - // Escape any spaces in the URL here + // Fix espacios en la URL result.imageUrl = Utils::String::replace(art.text().get(), " ", "%20"); - // Get the media type returned by ScreenScraper + // Tipo de imagen std::string media_type = art.attribute("format").value(); if (!media_type.empty()) result.imageType = "." + media_type; - // Ask for the same image, but with a smaller size, for the thumbnail displayed during scraping + // Thumbnail pequeño para el scraper result.thumbnailUrl = result.imageUrl + "&maxheight=250"; }else{ LOG(LogDebug) << "Failed to find media XML node with name=" << ssConfig.media_name; @@ -340,9 +364,6 @@ void ScreenScraperRequest::processList(const pugi::xml_document& xmldoc, std::ve ScreenScraperRequest::ScreenScraperConfig ssConfig; // limit the number of results per platform, not in total. - // otherwise if the first platform returns >= 7 games - // but the second platform contains the relevant game, - // the relevant result would not be shown. for (int i = 0; game && i < MAX_SCRAPER_RESULTS; i++) { std::string id = game.child("id").text().get(); diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index d01c432b27..e8999b6b3c 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -9,18 +9,28 @@ #include "Settings.h" #include "SystemData.h" #include "Window.h" +#include "LocaleES.h" +#include "Sound.h" // necesario para Sound::get (scroll directo de ) +#include "NavigationSounds.h" // NUEVO: helper central de sonidos // buffer values for scrolling velocity (left, stopped, right) -const int logoBuffersLeft[] = { -5, -2, -1 }; -const int logoBuffersRight[] = { 1, 2, 5 }; +const int logoBuffersLeft[] = { -5, -2, -1 }; +const int logoBuffersRight[] = { 1, 2, 5 }; -SystemView::SystemView(Window* window) : IList(window, LIST_SCROLL_STYLE_SLOW, LIST_ALWAYS_LOOP), - mViewNeedsReload(true), - mSystemInfo(window, "SYSTEM INFO", Font::get(FONT_SIZE_SMALL), 0x33333300, ALIGN_CENTER) +SystemView::SystemView(Window* window) : + IList(window, LIST_SCROLL_STYLE_SLOW, LIST_ALWAYS_LOOP), + mViewNeedsReload(true), + mSystemInfo(window, "SYSTEM INFO", Font::get(FONT_SIZE_SMALL), 0x33333300, ALIGN_CENTER) { + auto& loc = LocaleES::getInstance(); + loc.loadFromSettings(); + + mSystemInfo.setText(loc.translate("SYSTEM INFO")); + mCamOffset = 0; mExtrasCamOffset = 0; mExtrasFadeOpacity = 0.0f; + mScrollSnd.reset(); setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); populate(); @@ -40,17 +50,18 @@ void SystemView::populate() if((*it)->isVisible()) { Entry e; - e.name = (*it)->getName(); + e.name = (*it)->getName(); e.object = *it; // make logo const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); if(logoElem) { - std::string path = logoElem->get("path"); + std::string path = logoElem->get("path"); std::string defaultPath = logoElem->has("default") ? logoElem->get("default") : ""; - if((!path.empty() && ResourceManager::getInstance()->fileExists(path)) - || (!defaultPath.empty() && ResourceManager::getInstance()->fileExists(defaultPath))) + + if((!path.empty() && ResourceManager::getInstance()->fileExists(path)) || + (!defaultPath.empty() && ResourceManager::getInstance()->fileExists(defaultPath))) { ImageComponent* logo = new ImageComponent(mWindow, false, false); logo->setMaxSize(mCarousel.logoSize * mCarousel.logoScale); @@ -59,40 +70,57 @@ void SystemView::populate() e.data.logo = std::shared_ptr(logo); } } - if (!e.data.logo) + + if(!e.data.logo) { // no logo in theme; use text - TextComponent* text = new TextComponent(mWindow, + TextComponent* text = new TextComponent( + mWindow, (*it)->getName(), Font::get(FONT_SIZE_LARGE), 0x000000FF, ALIGN_CENTER); + text->setSize(mCarousel.logoSize * mCarousel.logoScale); - text->applyTheme((*it)->getTheme(), "system", "logoText", ThemeFlags::FONT_PATH | ThemeFlags::FONT_SIZE | ThemeFlags::COLOR | ThemeFlags::FORCE_UPPERCASE | ThemeFlags::LINE_SPACING | ThemeFlags::TEXT); + text->applyTheme( + (*it)->getTheme(), + "system", + "logoText", + ThemeFlags::FONT_PATH + | ThemeFlags::FONT_SIZE + | ThemeFlags::COLOR + | ThemeFlags::FORCE_UPPERCASE + | ThemeFlags::LINE_SPACING + | ThemeFlags::TEXT); + e.data.logo = std::shared_ptr(text); - if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) + if(mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) { text->setHorizontalAlignment(mCarousel.logoAlignment); text->setVerticalAlignment(ALIGN_CENTER); - } else { + } + else + { text->setHorizontalAlignment(ALIGN_CENTER); text->setVerticalAlignment(mCarousel.logoAlignment); } } - if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) + if(mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) { - if (mCarousel.logoAlignment == ALIGN_LEFT) + if(mCarousel.logoAlignment == ALIGN_LEFT) e.data.logo->setOrigin(0, 0.5); - else if (mCarousel.logoAlignment == ALIGN_RIGHT) + else if(mCarousel.logoAlignment == ALIGN_RIGHT) e.data.logo->setOrigin(1.0, 0.5); else e.data.logo->setOrigin(0.5, 0.5); - } else { - if (mCarousel.logoAlignment == ALIGN_TOP) + } + else + { + if(mCarousel.logoAlignment == ALIGN_TOP) e.data.logo->setOrigin(0.5, 0); - else if (mCarousel.logoAlignment == ALIGN_BOTTOM) + else if(mCarousel.logoAlignment == ALIGN_BOTTOM) e.data.logo->setOrigin(0.5, 1); else e.data.logo->setOrigin(0.5, 0.5); @@ -100,8 +128,9 @@ void SystemView::populate() Vector2f denormalized = mCarousel.logoSize * e.data.logo->getOrigin(); e.data.logo->setPosition(denormalized.x(), denormalized.y(), 0.0); + // delete any existing extras - for (auto extra : e.data.backgroundExtras) + for(auto extra : e.data.backgroundExtras) delete extra; e.data.backgroundExtras.clear(); @@ -109,24 +138,43 @@ void SystemView::populate() e.data.backgroundExtras = ThemeData::makeExtras((*it)->getTheme(), "system", mWindow); // sort the extras by z-index - std::stable_sort(e.data.backgroundExtras.begin(), e.data.backgroundExtras.end(), [](GuiComponent* a, GuiComponent* b) { - return b->getZIndex() > a->getZIndex(); - }); + std::stable_sort( + e.data.backgroundExtras.begin(), + e.data.backgroundExtras.end(), + [](GuiComponent* a, GuiComponent* b) + { + return b->getZIndex() > a->getZIndex(); + }); this->add(e); } } - if (mEntries.size() == 0) + + if(mEntries.size() == 0) { // Something is wrong, there is not a single system to show, check if UI mode is not full - if (!UIModeController::getInstance()->isUIModeFull()) + if(!UIModeController::getInstance()->isUIModeFull()) { Settings::getInstance()->setString("UIMode", "Full"); - mWindow->pushGui(new GuiMsgBox(mWindow, "The selected UI mode has nothing to show,\n returning to UI mode: FULL", "OK", nullptr)); + mWindow->pushGui(new GuiMsgBox( + mWindow, + "The selected UI mode has nothing to show,\n returning to UI mode: FULL", + "OK", + nullptr)); } } } +void SystemView::onShow() +{ + mShowing = true; +} + +void SystemView::onHide() +{ + mShowing = false; +} + void SystemView::goToSystem(SystemData* system, bool animate) { setCursor(system); @@ -135,43 +183,86 @@ void SystemView::goToSystem(SystemData* system, bool animate) finishAnimation(0); } +// helper local para reproducir scroll de sistema +inline void playSystemScrollSound(SystemData* sys, + const std::shared_ptr& scrollFromCarousel) +{ + // 1) prioridad: sonido definido en + if (scrollFromCarousel) + { + scrollFromCarousel->play(); + return; + } + + if (!sys) + return; + + const std::shared_ptr& theme = sys->getTheme(); + if (!theme) + return; + + // 2) Intentar esquema tipo Batocera: "systembrowse" → scroll de carrusel + auto snd = NavigationSounds::getFromTheme(theme, "systembrowse"); + if (!snd) + { + // 3) fallback genérico "scroll" + snd = NavigationSounds::getFromTheme(theme, "scroll"); + } + + if (snd) + snd->play(); +} + 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")) + if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && + input.id == SDLK_r && SDL_GetModState() & KMOD_LCTRL && + Settings::getInstance()->getBool("Debug")) { LOG(LogInfo) << " Reloading all"; ViewController::get()->reloadAll(); return true; } - switch (mCarousel.type) + bool moved = false; + + switch(mCarousel.type) { case VERTICAL: case VERTICAL_WHEEL: - if (config->isMappedLike("up", input)) + if(config->isMappedLike("up", input)) { - listInput(-1); + moved = listInput(-1); + if (moved) + playSystemScrollSound(getSelected(), mScrollSnd); return true; } - if (config->isMappedLike("down", input)) + if(config->isMappedLike("down", input)) { - listInput(1); + moved = listInput(1); + if (moved) + playSystemScrollSound(getSelected(), mScrollSnd); return true; } break; + case HORIZONTAL: case HORIZONTAL_WHEEL: default: - if (config->isMappedLike("left", input)) + if(config->isMappedLike("left", input)) { - listInput(-1); + moved = listInput(-1); + if (moved) + playSystemScrollSound(getSelected(), mScrollSnd); return true; } - if (config->isMappedLike("right", input)) + if(config->isMappedLike("right", input)) { - listInput(1); + moved = listInput(1); + if (moved) + playSystemScrollSound(getSelected(), mScrollSnd); return true; } break; @@ -179,25 +270,63 @@ bool SystemView::input(InputConfig* config, Input input) if(config->isMappedTo("a", input)) { + // --- SONIDO DE SELECT COMPATIBLE CON ESQUEMA BATOCERA --- + std::shared_ptr selectSnd; + + SystemData* sys = getSelected(); + if(sys != nullptr) + { + const std::shared_ptr& theme = sys->getTheme(); + if (theme) + { + // 1) esquema nuevo: buscar "select" (navigationsounds) + selectSnd = NavigationSounds::getFromTheme(theme, "select"); + + // 2) fallback: comportamiento viejo, elemento "selectSound" + if (!selectSnd && theme->hasView("system")) + { + const ThemeData::ThemeElement* selectElem = + theme->getElement("system", "selectSound", "sound"); + + if(selectElem && selectElem->has("path")) + { + std::string path = selectElem->get("path"); + if(!path.empty()) + selectSnd = Sound::get(path); + } + } + } + } + + if(selectSnd) + selectSnd->play(); + // ------------------------------------------------------------------------- + stopScrolling(); ViewController::get()->goToGameList(getSelected()); return true; } - if (config->isMappedTo("x", input)) + + if(config->isMappedTo("x", input)) { // get random system - // go to system setCursor(SystemData::getRandomSystem()); return true; } - }else{ + } + else + { if(config->isMappedLike("left", input) || - config->isMappedLike("right", input) || - config->isMappedLike("up", input) || - config->isMappedLike("down", input)) + config->isMappedLike("right", input) || + config->isMappedLike("up", input) || + config->isMappedLike("down", input)) listInput(0); + Scripting::fireEvent("system-select", this->IList::getSelected()->getName(), "input"); - if(!UIModeController::getInstance()->isUIModeKid() && config->isMappedTo("select", input) && Settings::getInstance()->getBool("ScreenSaverControls")) + + if(!UIModeController::getInstance()->isUIModeKid() && + config->isMappedTo("select", input) && + Settings::getInstance()->getBool("ScreenSaverControls")) { mWindow->startScreenSaver(); mWindow->renderScreenSaver(); @@ -224,121 +353,141 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) 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); + // shortest way to target + float endPos = target; // directamente + 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) - + // animar opacidad de mSystemInfo (fade out, espera, fade in) cancelAnimation(1); cancelAnimation(2); std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); - bool goFast = transition_style == "instant"; - const float infoStartOpacity = mSystemInfo.getOpacity() / 255.f; + bool goFast = transition_style == "instant"; + const float infoStartOpacity = mSystemInfo.getOpacity() / 255.f; Animation* infoFadeOut = new LambdaAnimation( - [infoStartOpacity, this] (float t) - { - mSystemInfo.setOpacity((unsigned char)(Math::lerp(infoStartOpacity, 0.f, t) * 255)); - }, (int)(infoStartOpacity * (goFast ? 10 : 150))); + [infoStartOpacity, this](float t) + { + mSystemInfo.setOpacity((unsigned char)(Math::lerp(infoStartOpacity, 0.f, t) * 255)); + }, + (int)(infoStartOpacity * (goFast ? 10 : 150))); unsigned int gameCount = getSelected()->getDisplayedGameCount(); - // also change the text after we've fully faded out - setAnimation(infoFadeOut, 0, [this, gameCount] { - std::stringstream ss; + // Localización + LocaleES& loc = LocaleES::getInstance(); - if (!getSelected()->isGameSystem()) - ss << "CONFIGURATION"; - else - ss << gameCount << " GAME" << (gameCount == 1 ? "" : "S") << " AVAILABLE"; + // cambiar el texto después del fade out + setAnimation( + infoFadeOut, + 0, + [this, gameCount, &loc]() + { + std::stringstream ss; + + if(!getSelected()->isGameSystem()) + { + ss << loc.translate("CONFIGURATION"); + } + else + { + ss << gameCount << " " + << loc.translate(gameCount == 1 ? "GAME" : "GAMES") + << " " + << loc.translate("AVAILABLE"); + } - mSystemInfo.setText(ss.str()); - }, false, 1); + mSystemInfo.setText(ss.str()); + }, + false, + 1); Animation* infoFadeIn = new LambdaAnimation( [this](float t) - { - mSystemInfo.setOpacity((unsigned char)(Math::lerp(0.f, 1.f, t) * 255)); - }, goFast ? 10 : 300); + { + mSystemInfo.setOpacity((unsigned char)(Math::lerp(0.f, 1.f, t) * 255)); + }, + goFast ? 10 : 300); - // wait 600ms to fade in setAnimation(infoFadeIn, goFast ? 0 : 2000, nullptr, false, 2); - // no need to animate transition, we're not going anywhere (probably mEntries.size() == 1) + // si no hay movimiento, no animar if(endPos == mCamOffset && endPos == mExtrasCamOffset) return; Animation* anim; bool move_carousel = Settings::getInstance()->getBool("MoveCarousel"); + if(transition_style == "fade") { float startExtrasFade = mExtrasFadeOpacity; anim = new LambdaAnimation( [this, startExtrasFade, startPos, endPos, posMax, move_carousel](float t) - { - t -= 1; - float f = Math::lerp(startPos, endPos, t*t*t + 1); - if(f < 0) - f += posMax; - if(f >= posMax) - f -= posMax; - - this->mCamOffset = move_carousel ? f : endPos; - - t += 1; - if(t < 0.3f) - this->mExtrasFadeOpacity = Math::lerp(0.0f, 1.0f, t / 0.3f + startExtrasFade); - else if(t < 0.7f) - this->mExtrasFadeOpacity = 1.0f; - else - this->mExtrasFadeOpacity = Math::lerp(1.0f, 0.0f, (t - 0.7f) / 0.3f); + { + t -= 1; + float f = Math::lerp(startPos, endPos, t * t * t + 1); + if(f < 0) + f += posMax; + if(f >= posMax) + f -= posMax; + + this->mCamOffset = move_carousel ? f : endPos; + + t += 1; + if(t < 0.3f) + this->mExtrasFadeOpacity = Math::lerp(0.0f, 1.0f, t / 0.3f + startExtrasFade); + else if(t < 0.7f) + this->mExtrasFadeOpacity = 1.0f; + else + this->mExtrasFadeOpacity = Math::lerp(1.0f, 0.0f, (t - 0.7f) / 0.3f); - if(t > 0.5f) - this->mExtrasCamOffset = endPos; + if(t > 0.5f) + this->mExtrasCamOffset = endPos; - }, 500); - } else if (transition_style == "slide") { - // slide + }, + 500); + } + else if(transition_style == "slide") + { anim = new LambdaAnimation( [this, startPos, endPos, posMax, move_carousel](float t) - { - t -= 1; - float f = Math::lerp(startPos, endPos, t*t*t + 1); - if(f < 0) - f += posMax; - if(f >= posMax) - f -= posMax; - - this->mCamOffset = move_carousel ? f : endPos; - this->mExtrasCamOffset = f; - }, 500); - } else { - // instant - anim = new LambdaAnimation( - [this, startPos, endPos, posMax, move_carousel ](float t) - { - t -= 1; - float f = Math::lerp(startPos, endPos, t*t*t + 1); - if(f < 0) - f += posMax; - if(f >= posMax) - f -= posMax; - - this->mCamOffset = move_carousel ? f : endPos; - this->mExtrasCamOffset = endPos; - }, move_carousel ? 500 : 1); + { + t -= 1; + float f = Math::lerp(startPos, endPos, t * t * t + 1); + if(f < 0) + f += posMax; + if(f >= posMax) + f -= posMax; + + this->mCamOffset = move_carousel ? f : endPos; + this->mExtrasCamOffset = f; + + }, + 500); } + else + { + anim = new LambdaAnimation( + [this, startPos, endPos, posMax, move_carousel](float t) + { + t -= 1; + float f = Math::lerp(startPos, endPos, t * t * t + 1); + if(f < 0) + f += posMax; + if(f >= posMax) + f -= posMax; + + this->mCamOffset = move_carousel ? f : endPos; + this->mExtrasCamOffset = endPos; + }, + move_carousel ? 500 : 1); + } setAnimation(anim, 0, nullptr, false, 0); } @@ -346,45 +495,49 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) void SystemView::render(const Transform4x4f& parentTrans) { if(size() == 0) - return; // nothing to render + return; Transform4x4f trans = getTransform() * parentTrans; auto systemInfoZIndex = mSystemInfo.getZIndex(); - auto minMax = std::minmax(mCarousel.zIndex, systemInfoZIndex); + auto minMax = std::minmax(mCarousel.zIndex, systemInfoZIndex); renderExtras(trans, INT16_MIN, minMax.first); renderFade(trans); - if (mCarousel.zIndex > mSystemInfo.getZIndex()) { + if(mCarousel.zIndex > mSystemInfo.getZIndex()) renderInfoBar(trans); - } else { + else renderCarousel(trans); - } renderExtras(trans, minMax.first, minMax.second); - if (mCarousel.zIndex > mSystemInfo.getZIndex()) { + if(mCarousel.zIndex > mSystemInfo.getZIndex()) renderCarousel(trans); - } else { + else renderInfoBar(trans); - } renderExtras(trans, minMax.second, INT16_MAX); } std::vector SystemView::getHelpPrompts() { + LocaleES& loc = LocaleES::getInstance(); + std::vector prompts; - if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) - prompts.push_back(HelpPrompt("up/down", "choose")); + if(mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) + prompts.push_back(HelpPrompt("up/down", loc.translate("CHOOSE"))); else - prompts.push_back(HelpPrompt("left/right", "choose")); - prompts.push_back(HelpPrompt("a", "select")); - prompts.push_back(HelpPrompt("x", "random")); + prompts.push_back(HelpPrompt("left/right", loc.translate("CHOOSE"))); - if (!UIModeController::getInstance()->isUIModeKid() && Settings::getInstance()->getBool("ScreenSaverControls")) - prompts.push_back(HelpPrompt("select", "launch screensaver")); + prompts.push_back(HelpPrompt("a", loc.translate("SELECT"))); + prompts.push_back(HelpPrompt("x", loc.translate("RANDOM"))); + + if(!UIModeController::getInstance()->isUIModeKid() && + Settings::getInstance()->getBool("ScreenSaverControls")) + { + prompts.push_back(HelpPrompt("select", loc.translate("LAUNCH SCREENSAVER"))); + } return prompts; } @@ -396,7 +549,7 @@ HelpStyle SystemView::getHelpStyle() return style; } -void SystemView::onThemeChanged(const std::shared_ptr& /*theme*/) +void SystemView::onThemeChanged(const std::shared_ptr& /*theme*/) { LOG(LogDebug) << "SystemView::onThemeChanged()"; mViewNeedsReload = true; @@ -404,21 +557,21 @@ void SystemView::onThemeChanged(const std::shared_ptr& /*theme*/) } // Get the ThemeElements that make up the SystemView. -void SystemView::getViewElements(const std::shared_ptr& theme) +void SystemView::getViewElements(const std::shared_ptr& theme) { LOG(LogDebug) << "SystemView::getViewElements()"; getDefaultElements(); - if (!theme->hasView("system")) + if(!theme->hasView("system")) return; const ThemeData::ThemeElement* carouselElem = theme->getElement("system", "systemcarousel", "carousel"); - if (carouselElem) + if(carouselElem) getCarouselFromTheme(carouselElem); const ThemeData::ThemeElement* sysInfoElem = theme->getElement("system", "systemInfo", "text"); - if (sysInfoElem) + if(sysInfoElem) mSystemInfo.applyTheme(theme, "system", "systemInfo", ThemeFlags::ALL); mViewNeedsReload = false; @@ -427,109 +580,215 @@ void SystemView::getViewElements(const std::shared_ptr& theme) // Render system carousel void SystemView::renderCarousel(const Transform4x4f& trans) { + if(mEntries.empty()) + return; + // background box behind logos Transform4x4f carouselTrans = trans; carouselTrans.translate(Vector3f(mCarousel.pos.x(), mCarousel.pos.y(), 0.0)); - carouselTrans.translate(Vector3f(mCarousel.origin.x() * mCarousel.size.x() * -1, mCarousel.origin.y() * mCarousel.size.y() * -1, 0.0f)); + carouselTrans.translate(Vector3f( + mCarousel.origin.x() * mCarousel.size.x() * -1, + mCarousel.origin.y() * mCarousel.size.y() * -1, + 0.0f)); Vector2f clipPos(carouselTrans.translation().x(), carouselTrans.translation().y()); - Renderer::pushClipRect(Vector2i((int)clipPos.x(), (int)clipPos.y()), Vector2i((int)mCarousel.size.x(), (int)mCarousel.size.y())); + Renderer::pushClipRect( + Vector2i((int)clipPos.x(), (int)clipPos.y()), + Vector2i((int)mCarousel.size.x(), (int)mCarousel.size.y())); Renderer::setMatrix(carouselTrans); - Renderer::drawRect(0.0f, 0.0f, mCarousel.size.x(), mCarousel.size.y(), mCarousel.color, mCarousel.colorEnd, mCarousel.colorGradientHorizontal); + Renderer::drawRect( + 0.0f, + 0.0f, + mCarousel.size.x(), + mCarousel.size.y(), + mCarousel.color, + mCarousel.colorEnd, + mCarousel.colorGradientHorizontal); // draw logos - Vector2f logoSpacing(0.0, 0.0); // NB: logoSpacing will include the size of the logo itself as well! - float xOff = 0.0; - float yOff = 0.0; + Vector2f logoSpacing(0.0, 0.0); + float xOff = 0.0f; + float yOff = 0.0f; - switch (mCarousel.type) + switch(mCarousel.type) { - case VERTICAL_WHEEL: - yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2.f - (mCamOffset * logoSpacing[1]); - if (mCarousel.logoAlignment == ALIGN_LEFT) - xOff = mCarousel.logoSize.x() / 10.f; - else if (mCarousel.logoAlignment == ALIGN_RIGHT) - xOff = mCarousel.size.x() - (mCarousel.logoSize.x() * 1.1f); - else - xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2.f; - break; - case VERTICAL: - logoSpacing[1] = ((mCarousel.size.y() - (mCarousel.logoSize.y() * mCarousel.maxLogoCount)) / (mCarousel.maxLogoCount)) + mCarousel.logoSize.y(); - yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2.f - (mCamOffset * logoSpacing[1]); - - if (mCarousel.logoAlignment == ALIGN_LEFT) - xOff = mCarousel.logoSize.x() / 10.f; - else if (mCarousel.logoAlignment == ALIGN_RIGHT) - xOff = mCarousel.size.x() - (mCarousel.logoSize.x() * 1.1f); - else - xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2; - break; - case HORIZONTAL_WHEEL: - xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2 - (mCamOffset * logoSpacing[1]); - if (mCarousel.logoAlignment == ALIGN_TOP) - yOff = mCarousel.logoSize.y() / 10; - else if (mCarousel.logoAlignment == ALIGN_BOTTOM) - yOff = mCarousel.size.y() - (mCarousel.logoSize.y() * 1.1f); - else - yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2; - break; - case HORIZONTAL: - default: - logoSpacing[0] = ((mCarousel.size.x() - (mCarousel.logoSize.x() * mCarousel.maxLogoCount)) / (mCarousel.maxLogoCount)) + mCarousel.logoSize.x(); - xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2.f - (mCamOffset * logoSpacing[0]); - - if (mCarousel.logoAlignment == ALIGN_TOP) - yOff = mCarousel.logoSize.y() / 10.f; - else if (mCarousel.logoAlignment == ALIGN_BOTTOM) - yOff = mCarousel.size.y() - (mCarousel.logoSize.y() * 1.1f); - else - yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2.f; - break; + case VERTICAL_WHEEL: + yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2.f - (mCamOffset * logoSpacing[1]); + if(mCarousel.logoAlignment == ALIGN_LEFT) + xOff = mCarousel.logoSize.x() / 10.f; + else if(mCarousel.logoAlignment == ALIGN_RIGHT) + xOff = mCarousel.size.x() - (mCarousel.logoSize.x() * 1.1f); + else + xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2.f; + break; + + case VERTICAL: + logoSpacing[1] = ((mCarousel.size.y() - + (mCarousel.logoSize.y() * mCarousel.maxLogoCount)) / + (mCarousel.maxLogoCount)) + + mCarousel.logoSize.y(); + yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2.f + - (mCamOffset * logoSpacing[1]); + + if(mCarousel.logoAlignment == ALIGN_LEFT) + xOff = mCarousel.logoSize.x() / 10.f; + else if(mCarousel.logoAlignment == ALIGN_RIGHT) + xOff = mCarousel.size.x() - (mCarousel.logoSize.x() * 1.1f); + else + xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2; + break; + + case HORIZONTAL_WHEEL: + xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2 - (mCamOffset * logoSpacing[0]); + if(mCarousel.logoAlignment == ALIGN_TOP) + yOff = mCarousel.logoSize.y() / 10; + else if(mCarousel.logoAlignment == ALIGN_BOTTOM) + yOff = mCarousel.size.y() - (mCarousel.logoSize.y() * 1.1f); + else + yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2; + break; + + case HORIZONTAL: + default: + logoSpacing[0] = ((mCarousel.size.x() - + (mCarousel.logoSize.x() * mCarousel.maxLogoCount)) / + (mCarousel.maxLogoCount)) + + mCarousel.logoSize.x(); + xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2.f + - (mCamOffset * logoSpacing[0]); + + if(mCarousel.logoAlignment == ALIGN_TOP) + yOff = mCarousel.logoSize.y() / 10.f; + else if(mCarousel.logoAlignment == ALIGN_BOTTOM) + yOff = mCarousel.size.y() - (mCarousel.logoSize.y() * 1.1f); + else + yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2.f; + break; } - int center = (int)(mCamOffset); + int center = (int)(mCamOffset); int logoCount = Math::min(mCarousel.maxLogoCount, (int)mEntries.size()); // Adding texture loading buffers depending on scrolling speed and status int bufferIndex = getScrollingVelocity() + 1; - int bufferLeft = logoBuffersLeft[bufferIndex]; + if(bufferIndex < 0) bufferIndex = 0; + if(bufferIndex > 2) bufferIndex = 2; + + int bufferLeft = logoBuffersLeft[bufferIndex]; int bufferRight = logoBuffersRight[bufferIndex]; - if (logoCount == 1) + + if(logoCount == 1) { - bufferLeft = 0; + bufferLeft = 0; bufferRight = 0; } - for (int i = center - logoCount / 2 + bufferLeft; i <= center + logoCount / 2 + bufferRight; i++) + // lambda para dibujar un logo + auto renderLogo = [this, &carouselTrans, &logoSpacing, xOff, yOff](int i) { + if(mEntries.empty()) + return; + int index = i; - while (index < 0) + while(index < 0) index += (int)mEntries.size(); - while (index >= (int)mEntries.size()) + while(index >= (int)mEntries.size()) index -= (int)mEntries.size(); Transform4x4f logoTrans = carouselTrans; - logoTrans.translate(Vector3f(i * logoSpacing[0] + xOff, i * logoSpacing[1] + yOff, 0)); + + // Ajuste de separación cuando el logo central está escalado (solo HORIZONTAL) + if(mCarousel.type == HORIZONTAL && + mCarousel.logoScale != 1.0f && + mCarousel.scaledLogoSpacing != 0.0f) + { + float logoDiffX = ((logoSpacing[0] * mCarousel.logoScale) - logoSpacing[0]) + / 2.0f * mCarousel.scaledLogoSpacing; + + if(index == mCursor) + { + logoTrans.translate(Vector3f( + i * logoSpacing[0] + xOff, + i * logoSpacing[1] + yOff, + 0.0f)); + } + else if(i < mCursor || (mCursor == 0 && index > mCarousel.maxLogoCount)) + { + logoTrans.translate(Vector3f( + i * logoSpacing[0] + xOff - logoDiffX, + i * logoSpacing[1] + yOff, + 0.0f)); + } + else + { + logoTrans.translate(Vector3f( + i * logoSpacing[0] + xOff + logoDiffX, + i * logoSpacing[1] + yOff, + 0.0f)); + } + } + else + { + logoTrans.translate(Vector3f( + i * logoSpacing[0] + xOff, + i * logoSpacing[1] + yOff, + 0.0f)); + } float distance = i - mCamOffset; + // escala según distancia float scale = 1.0f + ((mCarousel.logoScale - 1.0f) * (1.0f - fabs(distance))); - scale = Math::min(mCarousel.logoScale, Math::max(1.0f, scale)); - scale /= mCarousel.logoScale; + scale = Math::min(mCarousel.logoScale, Math::max(1.0f, scale)); + scale /= mCarousel.logoScale; + + // opacidad mínima configurable + float minOpacity = mCarousel.minLogoOpacity; + if(minOpacity < 0.0f) minOpacity = 0.0f; + if(minOpacity > 1.0f) minOpacity = 1.0f; - int opacity = (int)Math::round(0x80 + ((0xFF - 0x80) * (1.0f - fabs(distance)))); - opacity = Math::max((int) 0x80, opacity); + int opref = (int)Math::round(minOpacity * 255.0f); + int opacity = (int)Math::round(opref + ((0xFF - opref) * (1.0f - fabs(distance)))); + if(opacity < opref) + opacity = opref; - const std::shared_ptr &comp = mEntries.at(index).data.logo; - if (mCarousel.type == VERTICAL_WHEEL || mCarousel.type == HORIZONTAL_WHEEL) { + const std::shared_ptr& comp = mEntries.at(index).data.logo; + if(!comp) + return; + + if(mCarousel.type == VERTICAL_WHEEL || mCarousel.type == HORIZONTAL_WHEEL) + { comp->setRotationDegrees(mCarousel.logoRotation * distance); comp->setRotationOrigin(mCarousel.logoRotationOrigin); } comp->setScale(scale); comp->setOpacity((unsigned char)opacity); comp->render(logoTrans); + }; + + // Primero todos menos el seleccionado + std::vector activePositions; + for(int i = center - logoCount / 2 + bufferLeft; + i <= center + logoCount / 2 + bufferRight; + i++) + { + int index = i; + while(index < 0) + index += (int)mEntries.size(); + while(index >= (int)mEntries.size()) + index -= (int)mEntries.size(); + + if(index == mCursor) + activePositions.push_back(i); + else + renderLogo(i); } + + // Luego el seleccionado, para vencer la superposición + for(auto activePos : activePositions) + renderLogo(activePos); + Renderer::popClipRect(); } @@ -549,42 +808,47 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp Renderer::pushClipRect(Vector2i::Zero(), Vector2i((int)mSize.x(), (int)mSize.y())); - for (int i = extrasCenter + logoBuffersLeft[bufferIndex]; i <= extrasCenter + logoBuffersRight[bufferIndex]; i++) + for(int i = extrasCenter + logoBuffersLeft[bufferIndex]; + i <= extrasCenter + logoBuffersRight[bufferIndex]; + i++) { int index = i; - while (index < 0) + while(index < 0) index += (int)mEntries.size(); - while (index >= (int)mEntries.size()) + while(index >= (int)mEntries.size()) index -= (int)mEntries.size(); - //Only render selected system when not showing - if (mShowing || index == mCursor) + // Only render selected system when not showing + if(mShowing || index == mCursor) { Transform4x4f extrasTrans = trans; - if (mCarousel.type == HORIZONTAL || mCarousel.type == HORIZONTAL_WHEEL) + if(mCarousel.type == HORIZONTAL || mCarousel.type == HORIZONTAL_WHEEL) extrasTrans.translate(Vector3f((i - mExtrasCamOffset) * mSize.x(), 0, 0)); else extrasTrans.translate(Vector3f(0, (i - mExtrasCamOffset) * mSize.y(), 0)); - Renderer::pushClipRect(Vector2i((int)extrasTrans.translation()[0], (int)extrasTrans.translation()[1]), - Vector2i((int)mSize.x(), (int)mSize.y())); + Renderer::pushClipRect( + Vector2i((int)extrasTrans.translation()[0], (int)extrasTrans.translation()[1]), + Vector2i((int)mSize.x(), (int)mSize.y())); + SystemViewData data = mEntries.at(index).data; - for (unsigned int j = 0; j < data.backgroundExtras.size(); j++) { - GuiComponent *extra = data.backgroundExtras[j]; - if (extra->getZIndex() >= lower && extra->getZIndex() < upper) { + for(unsigned int j = 0; j < data.backgroundExtras.size(); j++) + { + GuiComponent* extra = data.backgroundExtras[j]; + if(extra->getZIndex() >= lower && extra->getZIndex() < upper) extra->render(extrasTrans); - } } Renderer::popClipRect(); } } + Renderer::popClipRect(); } void SystemView::renderFade(const Transform4x4f& trans) { // fade extras if necessary - if (mExtrasFadeOpacity) + if(mExtrasFadeOpacity) { unsigned int fadeColor = 0x00000000 | (unsigned char)(mExtrasFadeOpacity * 255); Renderer::setMatrix(trans); @@ -593,31 +857,38 @@ void SystemView::renderFade(const Transform4x4f& trans) } // Populate the system carousel with the legacy values -void SystemView::getDefaultElements(void) +void SystemView::getDefaultElements(void) { // Carousel - mCarousel.type = HORIZONTAL; + mCarousel.type = HORIZONTAL; mCarousel.logoAlignment = ALIGN_CENTER; - mCarousel.size.x() = mSize.x(); - mCarousel.size.y() = 0.2325f * mSize.y(); - mCarousel.pos.x() = 0.0f; - mCarousel.pos.y() = 0.5f * (mSize.y() - mCarousel.size.y()); - mCarousel.origin.x() = 0.0f; - mCarousel.origin.y() = 0.0f; - mCarousel.color = 0xFFFFFFD8; - mCarousel.colorEnd = 0xFFFFFFD8; + mCarousel.size.x() = mSize.x(); + mCarousel.size.y() = 0.2325f * mSize.y(); + mCarousel.pos.x() = 0.0f; + mCarousel.pos.y() = 0.5f * (mSize.y() - mCarousel.size.y()); + mCarousel.origin.x() = 0.0f; + mCarousel.origin.y() = 0.0f; + mCarousel.color = 0xFFFFFFD8; + mCarousel.colorEnd = 0xFFFFFFD8; mCarousel.colorGradientHorizontal = true; - mCarousel.logoScale = 1.2f; - mCarousel.logoRotation = 7.5; - mCarousel.logoRotationOrigin.x() = -5; - mCarousel.logoRotationOrigin.y() = 0.5; - mCarousel.logoSize.x() = 0.25f * mSize.x(); - mCarousel.logoSize.y() = 0.155f * mSize.y(); - mCarousel.maxLogoCount = 3; - mCarousel.zIndex = 40; + mCarousel.logoScale = 1.2f; + mCarousel.logoRotation = 7.5f; + mCarousel.logoRotationOrigin.x() = -5.0f; + mCarousel.logoRotationOrigin.y() = 0.5f; + mCarousel.logoSize.x() = 0.25f * mSize.x(); + mCarousel.logoSize.y() = 0.155f * mSize.y(); + mCarousel.maxLogoCount = 3; + mCarousel.zIndex = 40; + + // valores por defecto para mejoras visuales + mCarousel.minLogoOpacity = 0.5f; // equivalente a 0x80 de antes + mCarousel.scaledLogoSpacing = 0.0f; // 0 = comportamiento clásico + + // sonido por defecto: ninguno (se rellena desde o NavigationSounds) + mScrollSnd.reset(); // System Info Bar - mSystemInfo.setSize(mSize.x(), mSystemInfo.getFont()->getLetterHeight()*2.2f); + mSystemInfo.setSize(mSize.x(), mSystemInfo.getFont()->getLetterHeight() * 2.2f); mSystemInfo.setPosition(0, (mCarousel.pos.y() + mCarousel.size.y() - 0.2f)); mSystemInfo.setBackgroundColor(0xDDDDDDD8); mSystemInfo.setRenderBackground(true); @@ -629,65 +900,85 @@ void SystemView::getDefaultElements(void) void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) { - if (elem->has("type")) + if(elem->has("type")) { - if (!(elem->get("type").compare("vertical"))) + if(!(elem->get("type").compare("vertical"))) mCarousel.type = VERTICAL; - else if (!(elem->get("type").compare("vertical_wheel"))) + else if(!(elem->get("type").compare("vertical_wheel"))) mCarousel.type = VERTICAL_WHEEL; - else if (!(elem->get("type").compare("horizontal_wheel"))) + else if(!(elem->get("type").compare("horizontal_wheel"))) mCarousel.type = HORIZONTAL_WHEEL; else mCarousel.type = HORIZONTAL; } - if (elem->has("size")) + + if(elem->has("size")) mCarousel.size = elem->get("size") * mSize; - if (elem->has("pos")) + + if(elem->has("pos")) mCarousel.pos = elem->get("pos") * mSize; - if (elem->has("origin")) + + if(elem->has("origin")) mCarousel.origin = elem->get("origin"); - if (elem->has("color")) + + if(elem->has("color")) { - mCarousel.color = elem->get("color"); + mCarousel.color = elem->get("color"); mCarousel.colorEnd = mCarousel.color; } - if (elem->has("colorEnd")) + + if(elem->has("colorEnd")) mCarousel.colorEnd = elem->get("colorEnd"); - if (elem->has("gradientType")) + + if(elem->has("gradientType")) mCarousel.colorGradientHorizontal = !(elem->get("gradientType").compare("horizontal")); - if (elem->has("logoScale")) + + if(elem->has("logoScale")) mCarousel.logoScale = elem->get("logoScale"); - if (elem->has("logoSize")) + + if(elem->has("logoSize")) mCarousel.logoSize = elem->get("logoSize") * mSize; - if (elem->has("maxLogoCount")) + + if(elem->has("maxLogoCount")) mCarousel.maxLogoCount = (int)Math::round(elem->get("maxLogoCount")); - if (elem->has("zIndex")) + + if(elem->has("zIndex")) mCarousel.zIndex = elem->get("zIndex"); - if (elem->has("logoRotation")) + + if(elem->has("logoRotation")) mCarousel.logoRotation = elem->get("logoRotation"); - if (elem->has("logoRotationOrigin")) + + if(elem->has("logoRotationOrigin")) mCarousel.logoRotationOrigin = elem->get("logoRotationOrigin"); - if (elem->has("logoAlignment")) + + if(elem->has("logoAlignment")) { - if (!(elem->get("logoAlignment").compare("left"))) + if(!(elem->get("logoAlignment").compare("left"))) mCarousel.logoAlignment = ALIGN_LEFT; - else if (!(elem->get("logoAlignment").compare("right"))) + else if(!(elem->get("logoAlignment").compare("right"))) mCarousel.logoAlignment = ALIGN_RIGHT; - else if (!(elem->get("logoAlignment").compare("top"))) + else if(!(elem->get("logoAlignment").compare("top"))) mCarousel.logoAlignment = ALIGN_TOP; - else if (!(elem->get("logoAlignment").compare("bottom"))) + else if(!(elem->get("logoAlignment").compare("bottom"))) mCarousel.logoAlignment = ALIGN_BOTTOM; else mCarousel.logoAlignment = ALIGN_CENTER; } -} -void SystemView::onShow() -{ - mShowing = true; -} + // lectura de propiedades extra + if(elem->has("minLogoOpacity")) + mCarousel.minLogoOpacity = elem->get("minLogoOpacity"); -void SystemView::onHide() -{ - mShowing = false; + if(elem->has("scaledLogoSpacing")) + mCarousel.scaledLogoSpacing = elem->get("scaledLogoSpacing"); + + // sonido opcional para scroll del carrusel, definido dentro del + if(elem->has("scrollSound")) + { + std::string path = elem->get("scrollSound"); + if(!path.empty()) + { + mScrollSnd = Sound::get(path); // registra internamente en AudioManager + } + } } diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index 85b0aca44f..de6bc8e2ba 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -6,7 +6,9 @@ #include "components/TextComponent.h" #include "resources/Font.h" #include "GuiComponent.h" +#include "Sound.h" // ← para std::shared_ptr #include +#include class AnimatedImageComponent; class SystemData; @@ -41,8 +43,13 @@ struct SystemViewCarousel int maxLogoCount; // number of logos shown on the carousel Vector2f logoSize; float zIndex; + + // NUEVO: propiedades mejoradas para comportamiento visual + float minLogoOpacity; // Opacidad mínima (0.0f–1.0f) + float scaledLogoSpacing; // Ajuste de separación cuando el logo central está escalado }; + class SystemView : public IList { public: @@ -76,7 +83,6 @@ class SystemView : public IList void renderInfoBar(const Transform4x4f& trans); void renderFade(const Transform4x4f& trans); - SystemViewCarousel mCarousel; TextComponent mSystemInfo; @@ -87,6 +93,10 @@ class SystemView : public IList bool mViewNeedsReload; bool mShowing; + + // NUEVO: sonido opcional al mover el carrusel + // Se carga desde scrollSound del . + std::shared_ptr mScrollSnd; }; #endif // ES_APP_VIEWS_SYSTEM_VIEW_H diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 3d5079acec..0c26c87126 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -17,6 +17,13 @@ #include "Settings.h" #include "SystemData.h" #include "Window.h" +#include "Sound.h" + +// ✅ Popup concreto (implementa Window::InfoPopup) +#include "guis/GuiInfoPopup.h" + +// 🔊 NUEVO: música de fondo ES-X +#include "audio/BackgroundMusicManager.h" ViewController* ViewController::sInstance = NULL; @@ -96,6 +103,15 @@ void ViewController::goToSystemView(SystemData* system) mCurrentView->onHide(); } + // 🔊 SONIDO DE BACK: + // Solo reproducir si venimos de una gamelist (volver al carrusel) + if (mState.viewing == GAME_LIST) + { + auto backSound = Sound::getFromTheme(system->getTheme(), "system", "backSound"); + if (backSound) + backSound->play(); + } + mState.viewing = SYSTEM_SELECT; mState.system = system; @@ -274,6 +290,9 @@ void ViewController::launch(FileData* game, Vector3f center) mWindow->stopInfoPopup(); // make sure we disable any existing info popup mLockInput = true; + // 🔊 NUEVO: avisar que se lanza un juego (detener música de fondo) + BackgroundMusicManager::getInstance().onGameLaunched(); + std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); if(transition_style == "fade") { @@ -284,6 +303,10 @@ void ViewController::launch(FileData* game, Vector3f center) setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this, game, fadeFunc] { game->launchGame(mWindow); + + // 🔊 NUEVO: al volver del juego → reanudar música si corresponde + BackgroundMusicManager::getInstance().onGameEnded(); + setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this, game] { mLockInput = false; }, true); this->onFileChanged(game, FILE_METADATA_CHANGED); if (mCurrentView) { @@ -296,6 +319,10 @@ void ViewController::launch(FileData* game, Vector3f center) setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 1500), 0, [this, origCamera, center, game] { game->launchGame(mWindow); + + // 🔊 NUEVO: al volver del juego + BackgroundMusicManager::getInstance().onGameEnded(); + mCamera = origCamera; setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 600), 0, [this, game] { mLockInput = false; }, true); this->onFileChanged(game, FILE_METADATA_CHANGED); @@ -308,6 +335,10 @@ void ViewController::launch(FileData* game, Vector3f center) setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 10), 0, [this, origCamera, center, game] { game->launchGame(mWindow); + + // 🔊 NUEVO: al volver del juego + BackgroundMusicManager::getInstance().onGameEnded(); + mCamera = origCamera; setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 10), 0, [this, game] { mLockInput = false; }, true); this->onFileChanged(game, FILE_METADATA_CHANGED); @@ -424,7 +455,6 @@ std::shared_ptr ViewController::getSystemListView() return mSystemListView; } - bool ViewController::input(InputConfig* config, Input input) { if(mLockInput) @@ -456,6 +486,20 @@ void ViewController::update(int deltaTime) mCurrentView->update(deltaTime); } + // ========================== + // 🎵 Popup “Now playing” + // ========================== + auto& bgm = BackgroundMusicManager::getInstance(); + if (bgm.songNameChanged()) + { + mWindow->stopInfoPopup(); + + // ✅ GuiInfoPopup implementa Window::InfoPopup en tu fork + mWindow->setInfoPopup(new GuiInfoPopup(mWindow, "♪♪" + bgm.getCurrentSongDisplayName(), 4000)); + + bgm.resetSongNameChangedFlag(); + } + updateSelf(deltaTime); } @@ -552,7 +596,6 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) // Redisplay the current view if (mCurrentView) mCurrentView->onShow(); - } void ViewController::reloadAll(bool themeChanged) diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 605c8bad2b..ae15af21cb 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -6,6 +6,56 @@ #include "CollectionSystemManager.h" #include "Settings.h" #include "SystemData.h" +#include "LocaleES.h" + +// NUEVO: soporte de sonidos de navegación +#include "Sound.h" +#include "ThemeData.h" + +namespace +{ + // Busca un ThemeElement de tipo "sound" por nombre, considerando + // (view="all") y vistas gamelist/system. + inline std::shared_ptr getNavSound(SystemData* sys, const std::string& name) + { + if (!sys) + return nullptr; + + const std::shared_ptr& theme = sys->getTheme(); + if (!theme) + return nullptr; + + const ThemeData::ThemeElement* elem = nullptr; + + // 1) Esquema Batocera: + // , , , etc. + elem = theme->getElement("all", name, "sound"); + + // 2) Por si el tema lo define en la vista "gamelist" + if (!elem && theme->hasView("gamelist")) + elem = theme->getElement("gamelist", name, "sound"); + + // 3) Fallback: vista "system" + if (!elem && theme->hasView("system")) + elem = theme->getElement("system", name, "sound"); + + if (!elem || !elem->has("path")) + return nullptr; + + std::string path = elem->get("path"); + if (path.empty()) + return nullptr; + + return Sound::get(path); + } + + inline void playNavSound(SystemData* sys, const std::string& name) + { + auto snd = getNavSound(sys, name); + if (snd) + snd->play(); + } +} BasicGameListView::BasicGameListView(Window* window, FileData* root) : ISimpleGameListView(window, root), mList(window) @@ -29,7 +79,7 @@ void BasicGameListView::onThemeChanged(const std::shared_ptr& theme) void BasicGameListView::onFileChanged(FileData* file, FileChangeType change) { - if(change == FILE_METADATA_CHANGED) + if (change == FILE_METADATA_CHANGED) { // might switch to a detailed view ViewController::get()->reloadGameListView(this); @@ -45,7 +95,7 @@ void BasicGameListView::populateList(const std::vector& files) mHeaderText.setText(mRoot->getSystem()->getFullName()); if (files.size() > 0) { - for(auto it = files.cbegin(); it != files.cend(); it++) + for (auto it = files.cbegin(); it != files.cend(); it++) { mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); } @@ -67,7 +117,7 @@ void BasicGameListView::setCursor(FileData* cursor, bool refreshListCursorPos) setViewportTop(mList.REFRESH_LIST_CURSOR_POS); bool notInList = !mList.setCursor(cursor); - if(!refreshListCursorPos && notInList && !cursor->isPlaceHolder()) + if (!refreshListCursorPos && notInList && !cursor->isPlaceHolder()) { populateList(cursor->getParent()->getChildrenListToDisplay()); // this extra call is needed iff a system has games organized in folders @@ -76,11 +126,11 @@ void BasicGameListView::setCursor(FileData* cursor, bool refreshListCursorPos) 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()) + if (mCursorStack.empty() || mCursorStack.top() != cursor->getParent()) { std::stack tmp; FileData* ptr = cursor->getParent(); - while(ptr && ptr != mRoot) + while (ptr && ptr != mRoot) { tmp.push(ptr); ptr = ptr->getParent(); @@ -88,7 +138,7 @@ void BasicGameListView::setCursor(FileData* cursor, bool refreshListCursorPos) // flip the stack and put it in mCursorStack mCursorStack = std::stack(); - while(!tmp.empty()) + while (!tmp.empty()) { mCursorStack.push(tmp.top()); tmp.pop(); @@ -102,7 +152,6 @@ void BasicGameListView::setViewportTop(int index) mList.setViewportTop(index); } - int BasicGameListView::getViewportTop() { return mList.getViewportTop(); @@ -110,8 +159,20 @@ int BasicGameListView::getViewportTop() void BasicGameListView::addPlaceholder() { + LocaleES& loc = LocaleES::getInstance(); + + // Intentamos traducir; si no hay traducción, dejamos el texto por defecto. + std::string placeholderName = loc.translate("NO ENTRIES FOUND"); + if (placeholderName == "NO ENTRIES FOUND") + placeholderName = ""; + // empty list - add a placeholder - FileData* placeholder = new FileData(PLACEHOLDER, "", this->mRoot->getSystem()->getSystemEnvData(), this->mRoot->getSystem()); + FileData* placeholder = new FileData( + PLACEHOLDER, + placeholderName, + this->mRoot->getSystem()->getSystemEnvData(), + this->mRoot->getSystem()); + mList.add(placeholder->getName(), placeholder, (placeholder->getType() == PLACEHOLDER)); } @@ -127,6 +188,10 @@ std::string BasicGameListView::getQuickSystemSelectLeftButton() void BasicGameListView::launch(FileData* game) { + // SONIDO DE LAUNCH (Batocera-style) + if (game) + playNavSound(game->getSystem(), "launch"); + ViewController::get()->launch(game); } @@ -191,42 +256,85 @@ void BasicGameListView::remove(FileData *game, bool deleteFile, bool refreshView if ((gamePos + 1) < siblings.size()) { setCursor(siblings.at(gamePos + 1)); - } else if (gamePos > 1) { + } + else if (gamePos > 1) { setCursor(siblings.at(gamePos - 1)); } } } mList.remove(game); - if(mList.size() == 0) + if (mList.size() == 0) { addPlaceholder(); } delete game; // remove before repopulating (removes from parent) - if(refreshView) + if (refreshView) onFileChanged(parent, FILE_REMOVED); // update the view, with game removed } std::vector BasicGameListView::getHelpPrompts() { + LocaleES& loc = LocaleES::getInstance(); + std::vector 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")); - if(!UIModeController::getInstance()->isUIModeKid()) - prompts.push_back(HelpPrompt("select", "options")); - if(mRoot->getSystem()->isGameSystem()) - prompts.push_back(HelpPrompt("x", "random")); - if(mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) + if (Settings::getInstance()->getBool("QuickSystemSelect")) + { + // "SYSTEM" no está en tu .ini aún, pero si no existe saldrá "SYSTEM" + prompts.push_back(HelpPrompt("left/right", loc.translate("SYSTEM"))); + } + + prompts.push_back(HelpPrompt("up/down", loc.translate("CHOOSE"))); + prompts.push_back(HelpPrompt("a", loc.translate("START"))); + prompts.push_back(HelpPrompt("b", loc.translate("BACK"))); + + if (!UIModeController::getInstance()->isUIModeKid()) + { + // Puedes añadir OPTIONS=OPCIONES al .ini si quieres + prompts.push_back(HelpPrompt("select", loc.translate("OPTIONS"))); + } + + if (mRoot->getSystem()->isGameSystem()) + prompts.push_back(HelpPrompt("x", loc.translate("RANDOM"))); + + if (mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) { std::string prompt = CollectionSystemManager::get()->getEditingCollection(); prompts.push_back(HelpPrompt("y", prompt)); } + return prompts; } void BasicGameListView::onFocusLost() { mList.stopScrolling(true); } + +// NUEVO: interceptar input para reproducir BACK y FAVORITE +bool BasicGameListView::input(InputConfig* config, Input input) +{ + // Llamamos primero a la lógica base para no romper nada importante + if (ISimpleGameListView::input(config, input)) + return true; + + if (input.value != 0) + { + SystemData* sys = mRoot ? mRoot->getSystem() : nullptr; + + // BACK (B) + if (config->isMappedTo("b", input)) + { + playNavSound(sys, "back"); + return true; // evento consumido + } + + // FAVORITE / EDIT COLLECTION (Y) + if (config->isMappedTo("y", input)) + { + playNavSound(sys, "favorite"); + return true; // evento consumido + } + } + + return false; +} diff --git a/es-app/src/views/gamelist/BasicGameListView.h b/es-app/src/views/gamelist/BasicGameListView.h index afa9394c61..8dc1bffff7 100644 --- a/es-app/src/views/gamelist/BasicGameListView.h +++ b/es-app/src/views/gamelist/BasicGameListView.h @@ -25,13 +25,16 @@ class BasicGameListView : public ISimpleGameListView virtual std::vector getHelpPrompts() override; virtual void launch(FileData* game) override; - void onFocusLost() override; + // Necesario porque en el .cpp ya existe BasicGameListView::input(...) + virtual bool input(InputConfig* config, Input input) override; + + void onFocusLost() override; protected: virtual std::string getQuickSystemSelectRightButton() override; virtual std::string getQuickSystemSelectLeftButton() override; virtual void populateList(const std::vector& files) override; - virtual void remove(FileData* game, bool deleteFile, bool refreshView=true) override; + virtual void remove(FileData* game, bool deleteFile, bool refreshView = true) override; virtual void addPlaceholder(); TextListComponent mList; diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index ecac95c3bd..4a83c9d19e 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -2,6 +2,7 @@ #include "animations/LambdaAnimation.h" #include "views/ViewController.h" +#include "LocaleES.h" // ← NUEVO: sistema de idiomas DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : BasicGameListView(window, root), @@ -17,8 +18,6 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mGenre(window), mPlayers(window), mLastPlayed(window), mPlayCount(window), mName(window) { - //mHeaderImage.setPosition(mSize.x() * 0.25f, 0); - const float padding = 0.01f; mList.setPosition(mSize.x() * (0.50f + padding), mList.getPosition().y()); @@ -26,15 +25,14 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mList.setAlignment(TextListComponent::ALIGN_LEFT); mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); - // Image + // Imagen principal 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); + mImage.setMaxSize(mSize.x() * (0.50f - 2 * padding), mSize.y() * 0.4f); mImage.setDefaultZIndex(30); addChild(&mImage); - // Thumbnail - // Default to off the screen + // Thumbnail (por defecto fuera de pantalla) mThumbnail.setOrigin(0.5f, 0.5f); mThumbnail.setPosition(2.0f, 2.0f); mThumbnail.setMaxSize(mSize.x(), mSize.y()); @@ -42,8 +40,7 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mThumbnail.setVisible(false); addChild(&mThumbnail); - // Marquee - // Default to off the screen + // Marquee (por defecto fuera de pantalla) mMarquee.setOrigin(0.5f, 0.5f); mMarquee.setPosition(2.0f, 2.0f); mMarquee.setMaxSize(mSize.x(), mSize.y()); @@ -51,32 +48,43 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mMarquee.setVisible(false); addChild(&mMarquee); - // metadata labels + values - mLblRating.setText("Rating: "); + // --- Localización de etiquetas --- + LocaleES& loc = LocaleES::getInstance(); + loc.loadFromSettings(); // por si aún no se cargó en este contexto + + mLblRating.setText(loc.translate("RATING") + ": "); addChild(&mLblRating); addChild(&mRating); - mLblReleaseDate.setText("Released: "); + + mLblReleaseDate.setText(loc.translate("RELEASE DATE") + ": "); addChild(&mLblReleaseDate); addChild(&mReleaseDate); - mLblDeveloper.setText("Developer: "); + + mLblDeveloper.setText(loc.translate("DEVELOPER") + ": "); addChild(&mLblDeveloper); addChild(&mDeveloper); - mLblPublisher.setText("Publisher: "); + + mLblPublisher.setText(loc.translate("PUBLISHER") + ": "); addChild(&mLblPublisher); addChild(&mPublisher); - mLblGenre.setText("Genre: "); + + mLblGenre.setText(loc.translate("GENRE") + ": "); addChild(&mLblGenre); addChild(&mGenre); - mLblPlayers.setText("Players: "); + + mLblPlayers.setText(loc.translate("PLAYERS") + ": "); addChild(&mLblPlayers); addChild(&mPlayers); - mLblLastPlayed.setText("Last played: "); + + mLblLastPlayed.setText(loc.translate("LAST PLAYED") + ": "); addChild(&mLblLastPlayed); mLastPlayed.setDisplayRelative(true); addChild(&mLastPlayed); - mLblPlayCount.setText("Times played: "); + + mLblPlayCount.setText(loc.translate("PLAY COUNT") + ": "); addChild(&mLblPlayCount); addChild(&mPlayCount); + // --- fin localización de etiquetas --- mName.setPosition(mSize.x(), mSize.y()); mName.setDefaultZIndex(40); @@ -86,7 +94,7 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : addChild(&mName); mDescContainer.setPosition(mSize.x() * padding, mSize.y() * 0.65f); - mDescContainer.setSize(mSize.x() * (0.50f - 2*padding), mSize.y() - mDescContainer.getPosition().y()); + mDescContainer.setSize(mSize.x() * (0.50f - 2 * padding), mSize.y() - mDescContainer.getPosition().y()); mDescContainer.setAutoScroll(true); mDescContainer.setDefaultZIndex(40); addChild(&mDescContainer); @@ -95,7 +103,6 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mDescription.setSize(mDescContainer.getSize().x(), 0); mDescContainer.addChild(&mDescription); - initMDLabels(); initMDValues(); updateInfoPanel(); @@ -119,12 +126,11 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them "md_lbl_genre", "md_lbl_players", "md_lbl_lastplayed", "md_lbl_playcount" }; - for(unsigned int i = 0; i < labels.size(); i++) + for (unsigned int i = 0; i < labels.size(); i++) { labels[i]->applyTheme(theme, getName(), lblElements[i], ALL); } - initMDValues(); std::vector values = getMDValues(); assert(values.size() == 8); @@ -133,7 +139,7 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them "md_genre", "md_players", "md_lastplayed", "md_playcount" }; - for(unsigned int i = 0; i < values.size(); i++) + for (unsigned int i = 0; i < values.size(); i++) { values[i]->applyTheme(theme, getName(), valElements[i], ALL ^ ThemeFlags::TEXT); } @@ -157,16 +163,17 @@ void DetailedGameListView::initMDLabels() const float colSize = (mSize.x() * 0.48f) / colCount; const float rowPadding = 0.01f * mSize.y(); - for(unsigned int i = 0; i < components.size(); i++) + 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) + if (row == 0) { pos = start + Vector3f(colSize * (i / rowCount), 0, 0); - }else{ + } + else { // work from the last component - GuiComponent* lc = components[i-1]; + GuiComponent* lc = components[i - 1]; pos = lc->getPosition() + Vector3f(0, lc->getSize().y() + rowPadding, 0); } @@ -194,7 +201,7 @@ void DetailedGameListView::initMDValues() float bottom = 0.0f; const float colSize = (mSize.x() * 0.48f) / 2; - for(unsigned int i = 0; i < labels.size(); i++) + 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)); @@ -202,7 +209,7 @@ void DetailedGameListView::initMDValues() values[i]->setDefaultZIndex(40); float testBot = values[i]->getPosition().y() + values[i]->getSize().y(); - if(testBot > bottom) + if (testBot > bottom) bottom = testBot; } @@ -215,12 +222,11 @@ void DetailedGameListView::updateInfoPanel() FileData* file = (mList.size() == 0 || mList.isScrolling()) ? NULL : mList.getSelected(); bool fadingOut; - if(file == NULL) + if (file == NULL) { - //mImage.setImage(""); - //mDescription.setText(""); fadingOut = true; - }else{ + } + else { mThumbnail.setImage(file->getThumbnailPath()); mMarquee.setImage(file->getMarqueePath()); mImage.setImage(file->getImagePath()); @@ -235,7 +241,7 @@ void DetailedGameListView::updateInfoPanel() mPlayers.setValue(file->metadata.get("players")); mName.setValue(file->metadata.get("name")); - if(file->getType() == GAME) + if (file->getType() == GAME) { mLastPlayed.setValue(file->metadata.get("lastplayed")); mPlayCount.setValue(file->metadata.get("playcount")); @@ -253,19 +259,19 @@ void DetailedGameListView::updateInfoPanel() std::vector labels = getMDLabels(); comps.insert(comps.cend(), labels.cbegin(), labels.cend()); - for(auto it = comps.cbegin(); it != comps.cend(); it++) + for (auto it = comps.cbegin(); it != comps.cend(); 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) || + 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)(Math::lerp(0.0f, 1.0f, t)*255)); + comp->setOpacity((unsigned char)(Math::lerp(0.0f, 1.0f, t) * 255)); }; comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut); } @@ -275,7 +281,7 @@ void DetailedGameListView::updateInfoPanel() void DetailedGameListView::launch(FileData* game) { Vector3f target(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0); - if(mImage.hasImage()) + if (mImage.hasImage()) target = Vector3f(mImage.getCenter().x(), mImage.getCenter().y(), 0); ViewController::get()->launch(game, target); diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index c013fa3284..a6f8dee3c8 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -10,6 +10,7 @@ #include "components/VideoPlayerComponent.h" #endif #include "components/VideoVlcComponent.h" +#include "LocaleES.h" GridGameListView::GridGameListView(Window* window, FileData* root) : ISimpleGameListView(window, root), @@ -28,7 +29,10 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : { const float padding = 0.01f; -// Create the correct type of video window + // Localización + LocaleES& loc = LocaleES::getInstance(); + + // Create the correct type of video window #ifdef _OMX_ if (Settings::getInstance()->getBool("VideoOmxPlayer")) mVideo = new VideoPlayerComponent(window, ""); @@ -38,49 +42,61 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : mVideo = new VideoVlcComponent(window, getTitlePath()); #endif - mGrid.setPosition(mSize.x() * 0.1f, mSize.y() * 0.1f); + // GRID: columna izquierda (fallback sin tema) + mGrid.setPosition(mSize.x() * 0.05f, mSize.y() * 0.18f); + mGrid.setSize (mSize.x() * 0.55f, mSize.y() * 0.60f); mGrid.setDefaultZIndex(20); mGrid.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); addChild(&mGrid); populateList(root->getChildrenListToDisplay()); - // metadata labels + values - mLblRating.setText("Rating: "); + // metadata labels + values (localizados) + mLblRating.setText( loc.translate("RATING") + ": "); addChild(&mLblRating); addChild(&mRating); - mLblReleaseDate.setText("Released: "); + + mLblReleaseDate.setText( loc.translate("RELEASE DATE") + ": "); addChild(&mLblReleaseDate); addChild(&mReleaseDate); - mLblDeveloper.setText("Developer: "); + + mLblDeveloper.setText( loc.translate("DEVELOPER") + ": "); addChild(&mLblDeveloper); addChild(&mDeveloper); - mLblPublisher.setText("Publisher: "); + + mLblPublisher.setText( loc.translate("PUBLISHER") + ": "); addChild(&mLblPublisher); addChild(&mPublisher); - mLblGenre.setText("Genre: "); + + mLblGenre.setText( loc.translate("GENRE") + ": "); addChild(&mLblGenre); addChild(&mGenre); - mLblPlayers.setText("Players: "); + + mLblPlayers.setText( loc.translate("PLAYERS") + ": "); addChild(&mLblPlayers); addChild(&mPlayers); - mLblLastPlayed.setText("Last played: "); + + mLblLastPlayed.setText( loc.translate("LAST PLAYED") + ": "); addChild(&mLblLastPlayed); mLastPlayed.setDisplayRelative(true); addChild(&mLastPlayed); - mLblPlayCount.setText("Times played: "); + + mLblPlayCount.setText( loc.translate("PLAY COUNT") + ": "); addChild(&mLblPlayCount); addChild(&mPlayCount); - mName.setPosition(mSize.x(), mSize.y()); + // Título del juego: columna derecha, arriba + mName.setPosition(mSize.x() * 0.63f, mSize.y() * 0.18f); + mName.setSize(mSize.x() * 0.32f, 0); mName.setDefaultZIndex(40); mName.setColor(0xAAAAAAFF); mName.setFont(Font::get(FONT_SIZE_MEDIUM)); mName.setHorizontalAlignment(ALIGN_CENTER); addChild(&mName); - mDescContainer.setPosition(mSize.x() * padding, mSize.y() * 0.65f); - mDescContainer.setSize(mSize.x() * (0.50f - 2*padding), mSize.y() - mDescContainer.getPosition().y()); + // Descripción: columna derecha, debajo de la metadata + mDescContainer.setPosition(mSize.x() * 0.63f, mSize.y() * 0.44f); + mDescContainer.setSize(mSize.x() * 0.32f, mSize.y() - mDescContainer.getPosition().y()); mDescContainer.setAutoScroll(true); mDescContainer.setDefaultZIndex(40); addChild(&mDescContainer); @@ -90,7 +106,6 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : mDescContainer.addChild(&mDescription); // Image - // Default to off the screen mImage.setOrigin(0.5f, 0.5f); mImage.setPosition(2.0f, 2.0f); mImage.setMaxSize(mSize.x(), mSize.y()); @@ -99,7 +114,6 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : addChild(&mImage); // Video - // Default to off the screen mVideo->setOrigin(0.5f, 0.5f); mVideo->setPosition(2.0f, 2.0f); mVideo->setSize(mSize.x(), mSize.y()); @@ -108,7 +122,6 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : addChild(mVideo); // Marquee - // Default to off the screen mMarquee.setOrigin(0.5f, 0.5f); mMarquee.setPosition(2.0f, 2.0f); mMarquee.setMaxSize(mSize.x(), mSize.y()); @@ -212,7 +225,6 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) labels[i]->applyTheme(theme, getName(), lblElements[i], ALL); } - initMDValues(); std::vector values = getMDValues(); assert(values.size() == 8); @@ -245,9 +257,10 @@ void GridGameListView::initMDLabels() const unsigned int colCount = 2; const unsigned int rowCount = (int)(components.size() / 2); - Vector3f start(mSize.x() * 0.01f, mSize.y() * 0.625f, 0.0f); + // Tabla de metadata en la columna derecha + Vector3f start(mSize.x() * 0.63f, mSize.y() * 0.26f, 0.0f); - const float colSize = (mSize.x() * 0.48f) / colCount; + const float colSize = (mSize.x() * 0.35f) / colCount; const float rowPadding = 0.01f * mSize.y(); for(unsigned int i = 0; i < components.size(); i++) @@ -258,7 +271,6 @@ void GridGameListView::initMDLabels() { 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); } @@ -314,7 +326,6 @@ void GridGameListView::updateInfoPanel() mVideo->setImage(""); mVideoPlaying = false; - //mDescription.setText(""); fadingOut = true; }else{ if (!mVideo->setVideo(file->getVideoPath())) @@ -359,10 +370,6 @@ void GridGameListView::updateInfoPanel() for(auto it = comps.cbegin(); it != comps.cend(); 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))) { @@ -377,8 +384,18 @@ void GridGameListView::updateInfoPanel() void GridGameListView::addPlaceholder() { - // empty grid - add a placeholder - FileData* placeholder = new FileData(PLACEHOLDER, "", this->mRoot->getSystem()->getSystemEnvData(), this->mRoot->getSystem()); + LocaleES& loc = LocaleES::getInstance(); + + std::string placeholderName = loc.translate("NO ENTRIES FOUND"); + if (placeholderName == "NO ENTRIES FOUND") + placeholderName = ""; + + FileData* placeholder = new FileData( + PLACEHOLDER, + placeholderName, + this->mRoot->getSystem()->getSystemEnvData(), + this->mRoot->getSystem()); + mGrid.add(placeholder->getName(), "", placeholder); } @@ -408,15 +425,14 @@ void GridGameListView::launch(FileData* game) } ViewController::get()->launch(game, target); - } void GridGameListView::remove(FileData *game, bool deleteFile, bool refreshView) { if (deleteFile) - Utils::FileSystem::removeFile(game->getPath()); // actually delete the file on the filesystem + Utils::FileSystem::removeFile(game->getPath()); FileData* parent = game->getParent(); - if (getCursor() == game) // Select next element in list, or prev if none + if (getCursor() == game) { std::vector siblings = parent->getChildrenListToDisplay(); auto gameIter = std::find(siblings.cbegin(), siblings.cend(), game); @@ -436,10 +452,10 @@ void GridGameListView::remove(FileData *game, bool deleteFile, bool refreshView) { addPlaceholder(); } - delete game; // remove before repopulating (removes from parent) + delete game; if(refreshView) - onFileChanged(parent, FILE_REMOVED); // update the view, with game removed + onFileChanged(parent, FILE_REMOVED); } std::vector GridGameListView::getMDLabels() @@ -472,17 +488,20 @@ std::vector GridGameListView::getMDValues() std::vector GridGameListView::getHelpPrompts() { + LocaleES& loc = LocaleES::getInstance(); + std::vector prompts; if(Settings::getInstance()->getBool("QuickSystemSelect")) - prompts.push_back(HelpPrompt("lr", "system")); - prompts.push_back(HelpPrompt("up/down/left/right", "choose")); - prompts.push_back(HelpPrompt("a", "launch")); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("lr", loc.translate("SYSTEM"))); + + prompts.push_back(HelpPrompt("up/down/left/right", loc.translate("CHOOSE"))); + prompts.push_back(HelpPrompt("a", loc.translate("START"))); + prompts.push_back(HelpPrompt("b", loc.translate("BACK"))); if(!UIModeController::getInstance()->isUIModeKid()) - prompts.push_back(HelpPrompt("select", "options")); + prompts.push_back(HelpPrompt("select", loc.translate("OPTIONS"))); if(mRoot->getSystem()->isGameSystem()) - prompts.push_back(HelpPrompt("x", "random")); + prompts.push_back(HelpPrompt("x", loc.translate("RANDOM"))); if(mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) { std::string prompt = CollectionSystemManager::get()->getEditingCollection(); diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index c81ff0502f..d2d4402100 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -7,11 +7,18 @@ #include "Settings.h" #include "Sound.h" #include "SystemData.h" +#include "LocaleES.h" // soporte de idioma +#include "NavigationSounds.h" // helper central para navigationsounds -ISimpleGameListView::ISimpleGameListView(Window* window, FileData* root) : IGameListView(window, root), +ISimpleGameListView::ISimpleGameListView(Window* window, FileData* root) + : IGameListView(window, root), mHeaderText(window), mHeaderImage(window), mBackground(window) { - mHeaderText.setText("Logo Text"); + // Localización del texto por defecto del encabezado + LocaleES& loc = LocaleES::getInstance(); + loc.loadFromSettings(); + + mHeaderText.setText(loc.translate("LOGO TEXT")); // ← Traducción desde .ini mHeaderText.setSize(mSize.x(), 0); mHeaderText.setPosition(0, 0); mHeaderText.setHorizontalAlignment(ALIGN_CENTER); @@ -51,11 +58,14 @@ void ISimpleGameListView::onThemeChanged(const std::shared_ptr& theme addChild(extra); } - if(mHeaderImage.hasImage()) + // Mostrar imagen si está disponible, sino texto + if (mHeaderImage.hasImage()) { removeChild(&mHeaderText); addChild(&mHeaderImage); - }else{ + } + else + { addChild(&mHeaderText); removeChild(&mHeaderImage); } @@ -63,8 +73,6 @@ void ISimpleGameListView::onThemeChanged(const std::shared_ptr& theme 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(); if (!cursor->isPlaceHolder()) { populateList(cursor->getParent()->getChildrenListToDisplay()); @@ -79,36 +87,82 @@ void ISimpleGameListView::onFileChanged(FileData* /*file*/, FileChangeType /*cha bool ISimpleGameListView::input(InputConfig* config, Input input) { - if(input.value != 0) + if (input.value != 0) { + // LANZAR JUEGO / ENTRAR A CARPETA if(config->isMappedTo("a", input)) { FileData* cursor = getCursor(); if(cursor->getType() == GAME) { - Sound::getFromTheme(getTheme(), getName(), "launch")->play(); + // SONIDO DE LAUNCH (compatible con navigationsounds → "launch") + std::shared_ptr launchSnd; + + SystemData* sys = mRoot ? mRoot->getSystem() : nullptr; + if (sys != nullptr) + { + const std::shared_ptr& theme = sys->getTheme(); + if (theme) + { + // 1) esquema nuevo + launchSnd = NavigationSounds::getFromTheme(theme, "launch"); + } + } + + // 2) fallback clásico (por si acaso) + if (!launchSnd) + launchSnd = Sound::getFromTheme(getTheme(), getName(), "launch"); + + if (launchSnd) + launchSnd->play(); + launch(cursor); - }else{ - // it's a folder + } + else + { if(cursor->getChildren().size() > 0) { mCursorStack.push(cursor); populateList(cursor->getChildrenListToDisplay()); - FileData* cursor = getCursor(); - setCursor(cursor); + setCursor(getCursor()); } } return true; - }else if(config->isMappedTo("b", input)) + } + // VOLVER (dentro de carpetas o a SystemView) + else if(config->isMappedTo("b", input)) { + // 🎵 SONIDO DE BACK SIEMPRE (carpeta o salida a SystemView) + std::shared_ptr backSnd; + SystemData* sys = mRoot ? mRoot->getSystem() : nullptr; + if (sys != nullptr) + { + const std::shared_ptr& theme = sys->getTheme(); + if (theme) + { + // 1) esquema nuevo: navigationsounds → "back" + backSnd = NavigationSounds::getFromTheme(theme, "back"); + } + } + // 2) fallback clásico (por vista basic/detailed/video) + if (!backSnd) + backSnd = Sound::getFromTheme(getTheme(), getName(), "back"); + + if (backSnd) + backSnd->play(); + + // Lógica de navegación if(mCursorStack.size()) { + // Volver una carpeta atrás en la misma lista populateList(mCursorStack.top()->getParent()->getChildrenListToDisplay()); setCursor(mCursorStack.top()); mCursorStack.pop(); - Sound::getFromTheme(getTheme(), getName(), "back")->play(); - }else{ + } + else + { + // Salir a SystemView onFocusLost(); SystemData* systemToView = getCursor()->getSystem(); if (systemToView->isCollection()) @@ -119,27 +173,61 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } return true; - }else if(config->isMappedLike(getQuickSystemSelectRightButton(), input)) + } + // QUICK SYSTEM SELECT → LB/RB (sonido quicksysselect) + else if(config->isMappedLike(getQuickSystemSelectRightButton(), input)) { if(Settings::getInstance()->getBool("QuickSystemSelect")) { + // sonido quicksysselect (Batocera-like) + SystemData* sys = mRoot ? mRoot->getSystem() : nullptr; + if (sys != nullptr) + { + const std::shared_ptr& theme = sys->getTheme(); + if (theme) + { + auto snd = NavigationSounds::getFromTheme(theme, "quicksysselect"); + // fallback opcional al browse + if (!snd) + snd = NavigationSounds::getFromTheme(theme, "systembrowse"); + if (snd) + snd->play(); + } + } + onFocusLost(); ViewController::get()->goToNextGameList(); return true; } - }else if(config->isMappedLike(getQuickSystemSelectLeftButton(), input)) + } + else if(config->isMappedLike(getQuickSystemSelectLeftButton(), input)) { if(Settings::getInstance()->getBool("QuickSystemSelect")) { + SystemData* sys = mRoot ? mRoot->getSystem() : nullptr; + if (sys != nullptr) + { + const std::shared_ptr& theme = sys->getTheme(); + if (theme) + { + auto snd = NavigationSounds::getFromTheme(theme, "quicksysselect"); + if (!snd) + snd = NavigationSounds::getFromTheme(theme, "systembrowse"); + if (snd) + snd->play(); + } + } + onFocusLost(); ViewController::get()->goToPrevGameList(); return true; } - }else if (config->isMappedTo("x", input)) + } + // RANDOM + else if (config->isMappedTo("x", input)) { if (mRoot->getSystem()->isGameSystem()) { - // go to random system game FileData* randomGame = getCursor()->getSystem()->getRandomGame(); if (randomGame) { @@ -147,26 +235,44 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } return true; } - }else if (config->isMappedTo("y", input) && !UIModeController::getInstance()->isUIModeKid()) + } + // FAVORITO / COLECCIÓN (sonido "favorite") + else if (config->isMappedTo("y", input) && !UIModeController::getInstance()->isUIModeKid()) { if(mRoot->getSystem()->isGameSystem()) { if (CollectionSystemManager::get()->toggleGameInCollection(getCursor())) { + // sonido favorite al togglear colección (normalmente Favoritos) + SystemData* sys = mRoot ? mRoot->getSystem() : nullptr; + if (sys != nullptr) + { + const std::shared_ptr& theme = sys->getTheme(); + if (theme) + { + auto favSnd = NavigationSounds::getFromTheme(theme, "favorite"); + // fallback suave a "select" si el tema no tiene favorite + if (!favSnd) + favSnd = NavigationSounds::getFromTheme(theme, "select"); + if (favSnd) + favSnd->play(); + } + } return true; } } } } + // Evento game-select para scripts FileData* cursor = getCursor(); SystemData* system = this->mRoot->getSystem(); - if (system != NULL) { - Scripting::fireEvent("game-select", system->getName(), cursor->getPath(), cursor->getName(), "input"); - } + if (system != NULL) { + Scripting::fireEvent("game-select", system->getName(), cursor->getPath(), cursor->getName(), "input"); + } else { - Scripting::fireEvent("game-select", "NULL", "NULL", "NULL", "input"); + Scripting::fireEvent("game-select", "NULL", "NULL", "NULL", "input"); } return IGameListView::input(config, input); -} \ No newline at end of file +} diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index e26715c774..62614461b6 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -10,6 +10,12 @@ #ifdef _OMX_ #include "Settings.h" #endif +#include "LocaleES.h" // ← NUEVO: soporte de idioma + +// --- NUEVO: soporte de sonido launch --- +#include "Sound.h" +#include "SystemData.h" +// --- FIN NUEVO --- VideoGameListView::VideoGameListView(Window* window, FileData* root) : BasicGameListView(window, root), @@ -29,7 +35,7 @@ VideoGameListView::VideoGameListView(Window* window, FileData* root) : { const float padding = 0.01f; - // Create the correct type of video window + // Crear el tipo correcto de componente de video #ifdef _OMX_ Utils::FileSystem::removeFile(getTitlePath()); if (Settings::getInstance()->getBool("VideoOmxPlayer")) @@ -45,8 +51,8 @@ VideoGameListView::VideoGameListView(Window* window, FileData* root) : mList.setAlignment(TextListComponent::ALIGN_LEFT); mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); - // Image - // Default to off the screen + // Imagen + // Por defecto fuera de la pantalla mImage.setOrigin(0.5f, 0.5f); mImage.setPosition(2.0f, 2.0f); mImage.setMaxSize(mSize.x(), mSize.y()); @@ -57,12 +63,12 @@ VideoGameListView::VideoGameListView(Window* window, FileData* root) : // Video mVideo->setOrigin(0.5f, 0.5f); mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); - mVideo->setSize(mSize.x() * (0.5f - 2*padding), mSize.y() * 0.4f); + mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); mVideo->setDefaultZIndex(30); addChild(mVideo); // Thumbnail - // Default to off the screen + // Por defecto fuera de la pantalla mThumbnail.setOrigin(0.5f, 0.5f); mThumbnail.setPosition(2.0f, 2.0f); mThumbnail.setMaxSize(mSize.x(), mSize.y()); @@ -71,40 +77,51 @@ VideoGameListView::VideoGameListView(Window* window, FileData* root) : addChild(&mThumbnail); // Marquee - // Default to off the screen + // Por defecto fuera de la pantalla mMarquee.setOrigin(0.5f, 0.5f); mMarquee.setPosition(2.0f, 2.0f); mMarquee.setMaxSize(mSize.x(), mSize.y()); mMarquee.setDefaultZIndex(35); - mImage.setVisible(false); + mMarquee.setVisible(false); // ← FIX: antes estaba mImage.setVisible(false); addChild(&mMarquee); - // metadata labels + values - mLblRating.setText("Rating: "); + // --- LOCALIZACIÓN DE ETIQUETAS --- + LocaleES& loc = LocaleES::getInstance(); + loc.loadFromSettings(); + + mLblRating.setText(loc.translate("RATING") + ": "); addChild(&mLblRating); addChild(&mRating); - mLblReleaseDate.setText("Released: "); + + mLblReleaseDate.setText(loc.translate("RELEASE DATE") + ": "); addChild(&mLblReleaseDate); addChild(&mReleaseDate); - mLblDeveloper.setText("Developer: "); + + mLblDeveloper.setText(loc.translate("DEVELOPER") + ": "); addChild(&mLblDeveloper); addChild(&mDeveloper); - mLblPublisher.setText("Publisher: "); + + mLblPublisher.setText(loc.translate("PUBLISHER") + ": "); addChild(&mLblPublisher); addChild(&mPublisher); - mLblGenre.setText("Genre: "); + + mLblGenre.setText(loc.translate("GENRE") + ": "); addChild(&mLblGenre); addChild(&mGenre); - mLblPlayers.setText("Players: "); + + mLblPlayers.setText(loc.translate("PLAYERS") + ": "); addChild(&mLblPlayers); addChild(&mPlayers); - mLblLastPlayed.setText("Last played: "); + + mLblLastPlayed.setText(loc.translate("LAST PLAYED") + ": "); addChild(&mLblLastPlayed); mLastPlayed.setDisplayRelative(true); addChild(&mLastPlayed); - mLblPlayCount.setText("Times played: "); + + mLblPlayCount.setText(loc.translate("PLAY COUNT") + ": "); addChild(&mLblPlayCount); addChild(&mPlayCount); + // --- FIN LOCALIZACIÓN DE ETIQUETAS --- mName.setPosition(mSize.x(), mSize.y()); mName.setDefaultZIndex(40); @@ -114,7 +131,7 @@ VideoGameListView::VideoGameListView(Window* window, FileData* root) : addChild(&mName); mDescContainer.setPosition(mSize.x() * padding, mSize.y() * 0.65f); - mDescContainer.setSize(mSize.x() * (0.50f - 2*padding), mSize.y() - mDescContainer.getPosition().y()); + mDescContainer.setSize(mSize.x() * (0.50f - 2 * padding), mSize.y() - mDescContainer.getPosition().y()); mDescContainer.setAutoScroll(true); mDescContainer.setDefaultZIndex(40); addChild(&mDescContainer); @@ -151,12 +168,11 @@ void VideoGameListView::onThemeChanged(const std::shared_ptr& theme) "md_lbl_genre", "md_lbl_players", "md_lbl_lastplayed", "md_lbl_playcount" }; - for(unsigned int i = 0; i < labels.size(); i++) + for (unsigned int i = 0; i < labels.size(); i++) { labels[i]->applyTheme(theme, getName(), lblElements[i], ALL); } - initMDValues(); std::vector values = getMDValues(); assert(values.size() == 8); @@ -165,7 +181,7 @@ void VideoGameListView::onThemeChanged(const std::shared_ptr& theme) "md_genre", "md_players", "md_lastplayed", "md_playcount" }; - for(unsigned int i = 0; i < values.size(); i++) + for (unsigned int i = 0; i < values.size(); i++) { values[i]->applyTheme(theme, getName(), valElements[i], ALL ^ ThemeFlags::TEXT); } @@ -189,16 +205,17 @@ void VideoGameListView::initMDLabels() const float colSize = (mSize.x() * 0.48f) / colCount; const float rowPadding = 0.01f * mSize.y(); - for(unsigned int i = 0; i < components.size(); i++) + 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) + if (row == 0) { pos = start + Vector3f(colSize * (i / rowCount), 0, 0); - }else{ - // work from the last component - GuiComponent* lc = components[i-1]; + } + else { + // trabajar desde el último componente + GuiComponent* lc = components[i - 1]; pos = lc->getPosition() + Vector3f(0, lc->getSize().y() + rowPadding, 0); } @@ -226,7 +243,7 @@ void VideoGameListView::initMDValues() float bottom = 0.0f; const float colSize = (mSize.x() * 0.48f) / 2; - for(unsigned int i = 0; i < labels.size(); i++) + 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)); @@ -234,7 +251,7 @@ void VideoGameListView::initMDValues() values[i]->setDefaultZIndex(40); float testBot = values[i]->getPosition().y() + values[i]->getSize().y(); - if(testBot > bottom) + if (testBot > bottom) bottom = testBot; } @@ -242,14 +259,12 @@ void VideoGameListView::initMDValues() mDescContainer.setSize(mDescContainer.getSize().x(), mSize.y() - mDescContainer.getPosition().y()); } - - void VideoGameListView::updateInfoPanel() { FileData* file = (mList.size() == 0 || mList.isScrolling()) ? NULL : mList.getSelected(); bool fadingOut; - if(file == NULL) + if (file == NULL) { mVideo->setVideo(""); mVideo->setImage(""); @@ -258,7 +273,8 @@ void VideoGameListView::updateInfoPanel() //mDescription.setText(""); fadingOut = true; - }else{ + } + else { if (!mVideo->setVideo(file->getVideoPath())) { mVideo->setDefaultVideo(); @@ -281,7 +297,7 @@ void VideoGameListView::updateInfoPanel() mPlayers.setValue(file->metadata.get("players")); mName.setValue(file->metadata.get("name")); - if(file->getType() == GAME) + if (file->getType() == GAME) { mLastPlayed.setValue(file->metadata.get("lastplayed")); mPlayCount.setValue(file->metadata.get("playcount")); @@ -300,19 +316,19 @@ void VideoGameListView::updateInfoPanel() std::vector labels = getMDLabels(); comps.insert(comps.cend(), labels.cbegin(), labels.cend()); - for(auto it = comps.cbegin(); it != comps.cend(); it++) + for (auto it = comps.cbegin(); it != comps.cend(); 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) || + // si hay una animación corriendo: + // animar si reverse != fadingOut + // si no hay animación: + // animar si la opacidad actual no coincide con el objetivo + 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)(Math::lerp(0.0f, 1.0f, t)*255)); + comp->setOpacity((unsigned char)(Math::lerp(0.0f, 1.0f, t) * 255)); }; comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut); } @@ -321,37 +337,65 @@ void VideoGameListView::updateInfoPanel() void VideoGameListView::launch(FileData* game) { - float screenWidth = (float) Renderer::getScreenWidth(); - float screenHeight = (float) Renderer::getScreenHeight(); + // --- NUEVO: sonido de "launch" compatible con Batocera --- + if (game != nullptr) + { + SystemData* sys = game->getSystem(); + if (sys != nullptr) + { + const std::shared_ptr& theme = sys->getTheme(); + if (theme) + { + // Prioridad: + const ThemeData::ThemeElement* launchElem = + theme->getElement("all", "launch", "sound"); + + // Fallback opcional: por si alguien lo pone en la vista "system" + if (!launchElem) + launchElem = theme->getElement("system", "launch", "sound"); + + if (launchElem && launchElem->has("path")) + { + std::string path = launchElem->get("path"); + if (!path.empty()) + Sound::get(path)->play(); + } + } + } + } + // --- FIN NUEVO --- + + float screenWidth = (float)Renderer::getScreenWidth(); + float screenHeight = (float)Renderer::getScreenHeight(); Vector3f target(screenWidth / 2.0f, screenHeight / 2.0f, 0); - if(mMarquee.hasImage() && + if (mMarquee.hasImage() && (mMarquee.getPosition().x() < screenWidth && mMarquee.getPosition().x() > 0.0f && - mMarquee.getPosition().y() < screenHeight && mMarquee.getPosition().y() > 0.0f)) + mMarquee.getPosition().y() < screenHeight && mMarquee.getPosition().y() > 0.0f)) { target = Vector3f(mMarquee.getCenter().x(), mMarquee.getCenter().y(), 0); } - else if(mThumbnail.hasImage() && + else if (mThumbnail.hasImage() && (mThumbnail.getPosition().x() < screenWidth && mThumbnail.getPosition().x() > 2.0f && - mThumbnail.getPosition().y() < screenHeight && mThumbnail.getPosition().y() > 2.0f)) + mThumbnail.getPosition().y() < screenHeight && mThumbnail.getPosition().y() > 2.0f)) { target = Vector3f(mThumbnail.getCenter().x(), mThumbnail.getCenter().y(), 0); } - else if(mImage.hasImage() && + else if (mImage.hasImage() && (mImage.getPosition().x() < screenWidth && mImage.getPosition().x() > 2.0f && - mImage.getPosition().y() < screenHeight && mImage.getPosition().y() > 2.0f)) + mImage.getPosition().y() < screenHeight && mImage.getPosition().y() > 2.0f)) { target = Vector3f(mImage.getCenter().x(), mImage.getCenter().y(), 0); } - else if(mHeaderImage.hasImage() && + else if (mHeaderImage.hasImage() && (mHeaderImage.getPosition().x() < screenWidth && mHeaderImage.getPosition().x() > 0.0f && - mHeaderImage.getPosition().y() < screenHeight && mHeaderImage.getPosition().y() > 0.0f)) + mHeaderImage.getPosition().y() < screenHeight && mHeaderImage.getPosition().y() > 0.0f)) { target = Vector3f(mHeaderImage.getCenter().x(), mHeaderImage.getCenter().y(), 0); } - else if(mVideo->getPosition().x() < screenWidth && mVideo->getPosition().x() > 0.0f && - mVideo->getPosition().y() < screenHeight && mVideo->getPosition().y() > 0.0f) + else if (mVideo->getPosition().x() < screenWidth && mVideo->getPosition().x() > 0.0f && + mVideo->getPosition().y() < screenHeight && mVideo->getPosition().y() > 0.0f) { target = Vector3f(mVideo->getCenter().x(), mVideo->getCenter().y(), 0); } diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index c22459fd7c..73d989fa29 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -104,26 +104,27 @@ set(CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/animations/AnimationController.cpp # GuiComponents - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimatedImageComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ButtonComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentGrid.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentList.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/DateTimeComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/DateTimeEditComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/HelpComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GridTileComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ImageComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/MenuComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/NinePatchComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ScrollableContainer.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/SliderComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/SwitchComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextEditComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoPlayerComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoVlcComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimatedImageComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/ButtonComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentGrid.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentList.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/DateTimeComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/DateTimeEditComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/HelpComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/GridTileComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/ImageComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/MenuComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/NinePatchComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/ScrollableContainer.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/SliderComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/SwitchComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/MarqueeTextComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextEditComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoPlayerComponent.cpp +${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoVlcComponent.cpp # Guis ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiDetectDevice.cpp @@ -162,5 +163,8 @@ set(CORE_SOURCES ) include_directories(${COMMON_INCLUDE_DIRS}) +# ⬇⬇⬇ NUEVA LÍNEA - para poder usar LocaleES.h desde el core +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../es-app/src) + add_library(es-core STATIC ${CORE_SOURCES} ${CORE_HEADERS}) target_link_libraries(es-core ${COMMON_LIBRARIES}) diff --git a/es-core/src/InputManager.cpp b/es-core/src/InputManager.cpp index 67a8901c47..5f660fb4d4 100644 --- a/es-core/src/InputManager.cpp +++ b/es-core/src/InputManager.cpp @@ -6,6 +6,8 @@ #include "platform.h" #include "Scripting.h" #include "Window.h" +#include "guis/GuiInfoPopup.h" +#include "utils/StringUtil.h" #include #include #include @@ -275,9 +277,26 @@ bool InputManager::parseEvent(const SDL_Event& ev, Window* window) case SDL_JOYDEVICEADDED: addJoystickByDeviceIndex(ev.jdevice.which); // ev.jdevice.which is a device index + + // ★ Notificación (opcional): mando conectado + if (window != nullptr) + { + const char* joyNameC = SDL_JoystickNameForIndex(ev.jdevice.which); + std::string joyName = joyNameC ? std::string(joyNameC) : std::string("Controller"); + window->setInfoPopup(new GuiInfoPopup(window, std::string("★ Connected: ") + joyName, 4000)); + } return true; case SDL_JOYDEVICEREMOVED: + // ★ Notificación (opcional): mando desconectado + if (window != nullptr) + { + SDL_Joystick* joy = SDL_JoystickFromInstanceID(ev.jdevice.which); + const char* joyNameC = joy ? SDL_JoystickName(joy) : nullptr; + std::string joyName = joyNameC ? std::string(joyNameC) : std::string("Controller"); + window->setInfoPopup(new GuiInfoPopup(window, std::string("★ Disconnected: ") + joyName, 4000)); + } + removeJoystickByJoystickID(ev.jdevice.which); // ev.jdevice.which is an SDL_JoystickID (instance ID) return false; } diff --git a/es-core/src/LocaleESHook.h b/es-core/src/LocaleESHook.h new file mode 100644 index 0000000000..d1abac2fb9 --- /dev/null +++ b/es-core/src/LocaleESHook.h @@ -0,0 +1,6 @@ +#pragma once +#include + +// Función de traducción genérica usada desde es-core. +// La implementación real está en es-app (LocaleESHook.cpp). +std::string es_translate(const std::string& key); diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index aeb1057e33..4c984f60fd 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -1,3 +1,5 @@ +// ========== Settings.cpp (COMPLETO, VERSION ES-X CON EnableBGM) ========== + #include "Settings.h" #include "utils/FileSystemUtil.h" @@ -10,8 +12,7 @@ Settings* Settings::sInstance = NULL; -// these values are NOT saved to es_settings.xml -// since they're set through command-line arguments, and not the in-program settings menu +// Valores que NO se guardan en es_settings.cfg std::vector settings_dont_save { "Debug", "DebugGrid", @@ -47,7 +48,6 @@ Settings* Settings::getInstance() { if(sInstance == NULL) sInstance = new Settings(); - return sInstance; } @@ -79,6 +79,9 @@ void Settings::setDefaults() mBoolMap["QuickSystemSelect"] = true; mBoolMap["MoveCarousel"] = true; + // Modo oscuro ES-X + mBoolMap["MenuDark"] = false; + mBoolMap["ThreadedLoading"] = false; mBoolMap["Debug"] = false; @@ -86,16 +89,20 @@ void Settings::setDefaults() mBoolMap["DebugText"] = false; mBoolMap["DebugImage"] = false; + // 🆕 NUEVO: Música de fondo + mBoolMap["EnableBGM"] = true; + mIntMap["ScreenSaverTime"] = 5 * Settings::ONE_MINUTE_IN_MS; mIntMap["SystemSleepTime"] = 0 * Settings::ONE_MINUTE_IN_MS; mBoolMap["SystemSleepTimeHintDisplayed"] = false; mIntMap["ScraperResizeWidth"] = 400; mIntMap["ScraperResizeHeight"] = 0; - #ifdef _RPI_ - mIntMap["MaxVRAM"] = 80; - #else - mIntMap["MaxVRAM"] = 100; - #endif + +#ifdef _RPI_ + mIntMap["MaxVRAM"] = 80; +#else + mIntMap["MaxVRAM"] = 100; +#endif mStringMap["TransitionStyle"] = "fade"; mStringMap["ThemeSet"] = ""; @@ -118,35 +125,30 @@ void Settings::setDefaults() mStringMap["SlideshowScreenSaverVideoFilter"] = ".mp4,.avi"; mBoolMap["SlideshowScreenSaverRecurse"] = false; - // This setting only applies to raspberry pi but set it for all platforms so - // we don't get a warning if we encounter it on a different platform mBoolMap["VideoOmxPlayer"] = false; - #ifdef _OMX_ - // we're defaulting to OMX Player for full screen video on the Pi - mBoolMap["ScreenSaverOmxPlayer"] = true; - // use OMX Player defaults - mStringMap["SubtitleFont"] = "/usr/share/fonts/truetype/freefont/FreeSans.ttf"; - mStringMap["SubtitleItalicFont"] = "/usr/share/fonts/truetype/freefont/FreeSansOblique.ttf"; - mIntMap["SubtitleSize"] = 55; - mStringMap["SubtitleAlignment"] = "left"; - #else - mBoolMap["ScreenSaverOmxPlayer"] = false; - #endif +#ifdef _OMX_ + mBoolMap["ScreenSaverOmxPlayer"] = true; + mStringMap["SubtitleFont"] = "/usr/share/fonts/truetype/freefont/FreeSans.ttf"; + mStringMap["SubtitleItalicFont"] = "/usr/share/fonts/truetype/freefont/FreeSansOblique.ttf"; + mIntMap["SubtitleSize"] = 55; + mStringMap["SubtitleAlignment"] = "left"; +#else + mBoolMap["ScreenSaverOmxPlayer"] = false; +#endif mIntMap["ScreenSaverSwapVideoTimeout"] = 30000; mBoolMap["VideoAudio"] = true; mBoolMap["ScreenSaverVideoMute"] = false; mStringMap["VlcScreenSaverResolution"] = "original"; - // Audio out device for Video playback using OMX player. mStringMap["OMXAudioDev"] = "both"; - mIntMap["RandomCollectionMaxGames"] = 0; // 0 == no limit - std::map m1; - mMapIntMap["RandomCollectionSystemsAuto"] = m1; - std::map m2; - mMapIntMap["RandomCollectionSystemsCustom"] = m2; - std::map m3; - mMapIntMap["RandomCollectionSystems"] = m3; + + mIntMap["RandomCollectionMaxGames"] = 0; + std::map empty; + mMapIntMap["RandomCollectionSystemsAuto"] = empty; + mMapIntMap["RandomCollectionSystemsCustom"] = empty; + mMapIntMap["RandomCollectionSystems"] = empty; + mStringMap["RandomCollectionExclusionCollection"] = ""; mStringMap["CollectionSystemsAuto"] = ""; mStringMap["CollectionSystemsCustom"] = ""; @@ -158,12 +160,11 @@ void Settings::setDefaults() mBoolMap["LocalArt"] = false; - // Audio out device for volume control - #ifdef _RPI_ - mStringMap["AudioDevice"] = "HDMI"; - #else - mStringMap["AudioDevice"] = "Master"; - #endif +#ifdef _RPI_ + mStringMap["AudioDevice"] = "HDMI"; +#else + mStringMap["AudioDevice"] = "Master"; +#endif mStringMap["AudioCard"] = "default"; mStringMap["UIMode"] = "Full"; @@ -172,21 +173,22 @@ void Settings::setDefaults() mBoolMap["ForceKid"] = false; mBoolMap["ForceDisableFilters"] = false; - mIntMap["WindowWidth"] = 0; - mIntMap["WindowHeight"] = 0; - mIntMap["ScreenWidth"] = 0; - mIntMap["ScreenHeight"] = 0; + mIntMap["WindowWidth"] = 0; + mIntMap["WindowHeight"] = 0; + mIntMap["ScreenWidth"] = 0; + mIntMap["ScreenHeight"] = 0; mIntMap["ScreenOffsetX"] = 0; mIntMap["ScreenOffsetY"] = 0; - mIntMap["ScreenRotate"] = 0; + mIntMap["ScreenRotate"] = 0; mIntMap["MonitorID"] = 0; mBoolMap["UseFullscreenPaging"] = false; - mBoolMap["IgnoreLeadingArticles"] = false; - //No spaces! Order is important! - //"The A Squad" given [a,an,the] will sort as "A Squad", but given [the,a,an] will sort as "Squad" + mStringMap["LeadingArticles"] = "a,an,the"; + + // idioma por defecto ES-X + mStringMap["Language"] = "en"; } template @@ -194,7 +196,6 @@ void saveMap(pugi::xml_document& doc, std::map& map, const char* type) { for(auto iter = map.cbegin(); iter != map.cend(); iter++) { - // key is on the "don't save" list, so don't save it if(std::find(settings_dont_save.cbegin(), settings_dont_save.cend(), iter->first) != settings_dont_save.cend()) continue; @@ -211,11 +212,10 @@ void Settings::saveFile() pugi::xml_document doc; - saveMap(doc, mBoolMap, "bool"); - saveMap(doc, mIntMap, "int"); - saveMap(doc, mFloatMap, "float"); + saveMap(doc, mBoolMap, "bool"); + saveMap(doc, mIntMap, "int"); + saveMap(doc, mFloatMap, "float"); - //saveMap(doc, mStringMap, "string"); for(auto iter = mStringMap.cbegin(); iter != mStringMap.cend(); iter++) { pugi::xml_node node = doc.append_child("string"); @@ -229,7 +229,7 @@ void Settings::saveFile() node.append_attribute("name").set_value(m.first.c_str()); std::string datatype = "int"; node.append_attribute("type").set_value(datatype.c_str()); - for(auto &intMap : m.second) // intMap is a map + for(auto &intMap : m.second) { pugi::xml_node entry = node.append_child(datatype.c_str()); entry.append_attribute("name").set_value(intMap.first.c_str()); @@ -271,46 +271,44 @@ void Settings::loadFile() { std::string mapName = node.attribute("name").as_string(); std::string mapType = node.attribute("type").as_string(); - if (mapType == "int") { - // only supporting int value maps currently - std::map _map; + if(mapType == "int") + { + std::map _map; for(pugi::xml_node entry : node.children(mapType.c_str())) - { _map[entry.attribute("name").as_string()] = entry.attribute("value").as_int(); - } setMap(mapName, _map); - } else { - LOG(LogWarning) << "Map: '" << mapName << "'. Unsupported data type '"<< mapType <<"'. Value ignored!"; + } + else + { + LOG(LogWarning) << "Map '" << mapName << "' unsupported type '" << mapType << "'"; } } processBackwardCompatibility(); } - -void Settings::setMap(const std::string& key, const std::map& map) +void Settings::setMap(const std::string& key, const std::map& map) { mMapIntMap[key] = map; } -const std::map Settings::getMap(const std::string& key) +const std::map Settings::getMap(const std::string& key) { if(mMapIntMap.find(key) == mMapIntMap.cend()) { LOG(LogError) << "Tried to use undefined setting " << key << "!"; - std::map empty; + std::map empty; return empty; - } return mMapIntMap[key]; } - template void Settings::renameSetting(Map& map, std::string&& oldName, std::string&& newName) { - typename Map::const_iterator it = map.find(oldName); - if (it != map.end()) { + auto it = map.find(oldName); + if(it != map.end()) + { map[newName] = it->second; map.erase(it); } @@ -318,24 +316,30 @@ void Settings::renameSetting(Map& map, std::string&& oldName, std::string&& newN void Settings::processBackwardCompatibility() { - { // SaveGamelistsOnExit -> SaveGamelistsMode - std::map::const_iterator it = mBoolMap.find("SaveGamelistsOnExit"); - if (it != mBoolMap.end()) { + { + auto it = mBoolMap.find("SaveGamelistsOnExit"); + if(it != mBoolMap.end()) + { mStringMap["SaveGamelistsMode"] = it->second ? "on exit" : "never"; mBoolMap.erase(it); } } - { // ScreenSaverSlideShow Image -> Media - renameSetting>(mIntMap, std::string("ScreenSaverSwapImageTimeout"), std::string("ScreenSaverSwapMediaTimeout")); - renameSetting>(mBoolMap, std::string("SlideshowScreenSaverCustomImageSource"), std::string("SlideshowScreenSaverCustomMediaSource")); - renameSetting>(mStringMap, std::string("SlideshowScreenSaverImageDir"), std::string("SlideshowScreenSaverMediaDir")); - } -} + renameSetting>(mIntMap, + std::string("ScreenSaverSwapImageTimeout"), + std::string("ScreenSaverSwapMediaTimeout")); + renameSetting>(mBoolMap, + std::string("SlideshowScreenSaverCustomImageSource"), + std::string("SlideshowScreenSaverCustomMediaSource")); -//Print a warning message if the setting we're trying to get doesn't already exist in the map, then return the value in the map. -#define SETTINGS_GETSET(type, mapName, getMethodName, setMethodName) type Settings::getMethodName(const std::string& name) \ + renameSetting>(mStringMap, + std::string("SlideshowScreenSaverImageDir"), + std::string("SlideshowScreenSaverMediaDir")); +} + +#define SETTINGS_GETSET(type, mapName, getMethodName, setMethodName) \ +type Settings::getMethodName(const std::string& name) \ { \ if(mapName.find(name) == mapName.cend()) \ { \ @@ -352,3 +356,4 @@ SETTINGS_GETSET(bool, mBoolMap, getBool, setBool); SETTINGS_GETSET(int, mIntMap, getInt, setInt); SETTINGS_GETSET(float, mFloatMap, getFloat, setFloat); SETTINGS_GETSET(const std::string&, mStringMap, getString, setString); + diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index c223978352..9f3bbcecc2 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -2,6 +2,7 @@ #include "components/ImageComponent.h" #include "components/TextComponent.h" +#include "resources/ResourceManager.h" #include "utils/FileSystemUtil.h" #include "utils/StringUtil.h" #include "Log.h" @@ -9,10 +10,16 @@ #include "Settings.h" #include #include +#include +#include -std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" }, { "grid" }, { "video" } }; -std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" }, { "visible" } }; +// Vistas soportadas +std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" }, { "grid" }, { "video" }, { "all" } }; +// Features soportados +std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" }, { "visible" }, { "navigationsounds" } }; + +// Mapa de elementos y propiedades std::map> ThemeData::sElementMap { { "image", { { "pos", RESOLUTION_PAIR }, @@ -69,7 +76,13 @@ std::map> The { "lineSpacing", FLOAT }, { "value", STRING }, { "visible", BOOLEAN }, - { "zIndex", FLOAT } } }, + { "zIndex", FLOAT }, + + // NUEVO: borde de texto + { "textStrokeColor", COLOR }, + { "textStrokeSize", FLOAT } + } }, + { "textlist", { { "pos", RESOLUTION_PAIR }, { "size", RESOLUTION_PAIR }, @@ -169,14 +182,21 @@ std::map> The { "logoSize", NORMALIZED_PAIR }, { "logoAlignment", STRING }, { "maxLogoCount", FLOAT }, - { "zIndex", FLOAT } } } + { "zIndex", FLOAT }, + // NUEVO: propiedades extra del carrusel + { "minLogoOpacity", FLOAT }, // 0.0 – 1.0 + { "scaledLogoSpacing", FLOAT }, // factor de separación + { "scrollSound", PATH } // sonido opcional (por ahora solo parseado) + } } }; #define MINIMUM_THEME_FORMAT_VERSION 3 #define CURRENT_THEME_FORMAT_VERSION 6 -// helper -unsigned int getHexColor(const char* str) +// ------------------------------------------------------ +// helper: conversión de color "RRGGBB" o "RRGGBBAA" a int +// ------------------------------------------------------ +static unsigned int getHexColor(const char* str) { ThemeException error; if(!str) @@ -197,6 +217,149 @@ unsigned int getHexColor(const char* str) return val; } +// ───────────────────────────────────────────── +// NUEVO: carga de variables externas desde theme.ini +// con soporte de secciones y layouts +// ───────────────────────────────────────────── +namespace +{ + void loadExternalThemeVariables(const std::string& themeXmlPath, + std::map& outVars) + { + // Layout activo elegido por el usuario (ej: "smd", "layout_smd", etc.) + const std::string activeLayoutSetting = Settings::getInstance()->getString("ThemeLayout"); + const bool hasActiveLayout = !activeLayoutSetting.empty(); + const std::string activeLower = Utils::String::toLower(activeLayoutSetting); + + // Carpeta del theme.xml y sus padres + const std::string xmlDir = Utils::FileSystem::getParent(themeXmlPath); + const std::string parent1 = Utils::FileSystem::getParent(xmlDir); + const std::string parent2 = Utils::FileSystem::getParent(parent1); + + // Rutas candidatas para theme.ini + std::vector candidates; + candidates.push_back(xmlDir + "/theme.ini"); + candidates.push_back(parent1 + "/theme.ini"); + candidates.push_back(parent2 + "/theme.ini"); + + for(const auto& cfgPath : candidates) + { + if(!Utils::FileSystem::exists(cfgPath)) + continue; + + std::ifstream file(cfgPath.c_str()); + if(!file.good()) + { + LOG(LogWarning) << "ThemeData: could not open theme.ini at " << cfgPath; + continue; + } + + LOG(LogInfo) << "ThemeData: loading variables from " << cfgPath; + + std::string line; + std::string currentSection; // "", "global", "layout_smd", etc. + + // Si NO hay ThemeLayout definido, usaremos SOLO la primera sección "layout_..." + bool layoutAlreadyChosen = false; + + while(std::getline(file, line)) + { + line = Utils::String::trim(line); + + // saltar líneas vacías y comentarios + if(line.empty() || line[0] == '#' || line[0] == ';') + continue; + + // Sección INI: [nombre] + if(line.front() == '[' && line.back() == ']' && line.size() >= 2) + { + currentSection = Utils::String::trim(line.substr(1, line.size() - 2)); + continue; + } + + // Clave = valor + auto eqPos = line.find('='); + if(eqPos == std::string::npos) + continue; + + std::string key = Utils::String::trim(line.substr(0, eqPos)); + std::string value = Utils::String::trim(line.substr(eqPos + 1)); + + if(key.empty()) + continue; + + // ¿Aplicamos esta línea? + // - sin sección -> siempre + // - [global] / [default] -> siempre + // - [layout_xyz] con layout -> si coincide con ThemeLayout (flexible) + // - [layout_xyz] sin layout -> solo la PRIMER sección "layout_..." del archivo + bool isGlobalSection = + currentSection.empty() + || Utils::String::toLower(currentSection) == "global" + || Utils::String::toLower(currentSection) == "default"; + + bool matchesLayout = false; + + if(!currentSection.empty()) + { + std::string sectionLower = Utils::String::toLower(currentSection); + + bool isLayoutSection = + sectionLower.size() >= 7 && + sectionLower.substr(0, 7) == "layout_"; + + if(hasActiveLayout) + { + // Modo flexible: + // ThemeLayout = "smd" con sección "[layout_smd]" + // ThemeLayout = "layout_smd" con sección "[layout_smd]" + if(sectionLower == activeLower) + { + matchesLayout = true; + } + else if(isLayoutSection) + { + // sección "layout_xxx", layout "xxx" + std::string withoutPrefix = sectionLower.substr(7); + if(activeLower == withoutPrefix) + matchesLayout = true; + // sección "xxx", layout "layout_xxx" + if(!matchesLayout && activeLower.size() > 7 && + activeLower.substr(0,7) == "layout_" && + sectionLower == activeLower.substr(7)) + { + matchesLayout = true; + } + } + } + else + { + // No hay ThemeLayout: tomamos SOLO la primera sección "layout_..." + bool isLayoutSectionNoSetting = + sectionLower.size() >= 7 && + sectionLower.substr(0, 7) == "layout_"; + + if(isLayoutSectionNoSetting && !layoutAlreadyChosen) + { + matchesLayout = true; + layoutAlreadyChosen = true; + } + } + } + + if(isGlobalSection || matchesLayout) + { + // sobreescribe si ya existe (layout por encima de global) + outVars[key] = value; + } + } + + // Solo usamos el primer theme.ini encontrado + break; + } + } +} + std::string ThemeData::resolvePlaceholders(const char* in) { std::string inStr(in); @@ -214,7 +377,13 @@ std::string ThemeData::resolvePlaceholders(const char* in) std::string replace = inStr.substr(variableBegin + 2, variableEnd - (variableBegin + 2)); std::string suffix = resolvePlaceholders(inStr.substr(variableEnd + 1).c_str()); - return prefix + mVariables[replace] + suffix; + // Si la variable existe, la reemplazamos, si no, la dejamos en blanco + auto varIt = mVariables.find(replace); + std::string value; + if(varIt != mVariables.cend()) + value = varIt->second; + + return prefix + value + suffix; } ThemeData::ThemeData() @@ -238,8 +407,12 @@ void ThemeData::loadFile(std::map sysDataMap, const st mViews.clear(); mVariables.clear(); + // Variables del sistema (nombre, shortName, etc.) mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); + // NUEVO: variables externas desde theme.ini (global + layout activo) + loadExternalThemeVariables(path, mVariables); + pugi::xml_document doc; pugi::xml_parse_result res = doc.load_file(path.c_str()); if(!res) @@ -417,7 +590,6 @@ void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) } } - void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element) { ThemeException error; @@ -517,7 +689,7 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapfileExists(path)) { std::stringstream ss; - ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda + ss << " Warning " << error.msg; ss << "could not find file \"" << node.text().get() << "\" "; if(node.text().get() != path) ss << "(which resolved to \"" << path << "\") "; @@ -671,7 +843,10 @@ std::string ThemeData::getThemeFromCurrentSet(const std::string& system) return ""; } - std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + // usar siempre const_iterator para evitar el error de asignación + std::map::const_iterator set = + themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if(set == themeSets.cend()) { // currently selected theme set is missing, so just pick the first available set diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 42c9bfa746..6698aa0190 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -6,15 +6,33 @@ #include "resources/TextureResource.h" #include "Log.h" #include "Scripting.h" + #include #include +#include +#include #ifdef WIN32 #include #endif -Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), - mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) +Window::Window() + : mNormalizeNextUpdate(false) + , mFrameTimeElapsed(0) + , mFrameCountElapsed(0) + , mAverageDeltaTime(10) + , mAllowSleep(true) + , mSleeping(false) + , mTimeSinceLastInput(0) + , mScreenSaver(NULL) + , mRenderScreenSaver(false) + , mInfoPopup(NULL) + , mRenderedHelpPrompts(false) + // Clock + , mClockTimeAccum(0) + , mClockLastText("") + , mClockTextCache(nullptr) + , mClockOutlineCache(nullptr) { mHelp = new HelpComponent(this); mBackgroundOverlay = new ImageComponent(this); @@ -25,7 +43,7 @@ Window::~Window() delete mBackgroundOverlay; // delete all our GUIs - while(peekGui()) + while (peekGui()) delete peekGui(); delete mHelp; @@ -44,13 +62,13 @@ void Window::pushGui(GuiComponent* gui) void Window::removeGui(GuiComponent* gui) { - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + for (auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) { - if(*i == gui) + if (*i == gui) { i = mGuiStack.erase(i); - if(i == mGuiStack.cend() && mGuiStack.size()) // we just popped the stack and the stack is not empty + if (i == mGuiStack.cend() && mGuiStack.size()) // we just popped the stack and the stack is not empty { mGuiStack.back()->updateHelpPrompts(); mGuiStack.back()->topWindow(true); @@ -63,7 +81,7 @@ void Window::removeGui(GuiComponent* gui) GuiComponent* Window::peekGui() { - if(mGuiStack.size() == 0) + if (mGuiStack.size() == 0) return NULL; return mGuiStack.back(); @@ -71,7 +89,7 @@ GuiComponent* Window::peekGui() bool Window::init() { - if(!Renderer::init()) + if (!Renderer::init()) { LOG(LogError) << "Renderer failed to initialize!"; return false; @@ -79,8 +97,8 @@ bool Window::init() ResourceManager::getInstance()->reloadAll(); - //keep a reference to the default fonts, so they don't keep getting destroyed/recreated - if(mDefaultFonts.empty()) + // keep a reference to the default fonts, so they don't keep getting destroyed/recreated + if (mDefaultFonts.empty()) { mDefaultFonts.push_back(Font::get(FONT_SIZE_SMALL)); mDefaultFonts.push_back(Font::get(FONT_SIZE_MEDIUM)); @@ -91,16 +109,22 @@ bool Window::init() mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); // update our help because font sizes probably changed - if(peekGui()) + if (peekGui()) peekGui()->updateHelpPrompts(); + // Reset clock cache on init (safe) + mClockTimeAccum = 0; + mClockLastText.clear(); + mClockTextCache.reset(); + mClockOutlineCache.reset(); + return true; } void Window::deinit() { // Hide all GUI elements on uninitialisation - this disable - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + for (auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) { (*i)->onHide(); } @@ -110,7 +134,7 @@ void Window::deinit() void Window::textInput(const char* text) { - if(peekGui()) + if (peekGui()) peekGui()->textInput(text); } @@ -159,20 +183,20 @@ void Window::input(InputConfig* config, Input input) void Window::update(int deltaTime) { - if(mNormalizeNextUpdate) + if (mNormalizeNextUpdate) { mNormalizeNextUpdate = false; - if(deltaTime > mAverageDeltaTime) + if (deltaTime > mAverageDeltaTime) deltaTime = mAverageDeltaTime; } mFrameTimeElapsed += deltaTime; mFrameCountElapsed++; - if(mFrameTimeElapsed > 500) + if (mFrameTimeElapsed > 500) { mAverageDeltaTime = mFrameTimeElapsed / mFrameCountElapsed; - if(Settings::getInstance()->getBool("DrawFramerate")) + if (Settings::getInstance()->getBool("DrawFramerate")) { std::stringstream ss; @@ -186,7 +210,7 @@ void Window::update(int deltaTime) float fontVramUsageMb = Font::getTotalMemUsage() / 1000.0f / 1000.0f; ss << "\nFont VRAM: " << fontVramUsageMb << " Tex VRAM: " << textureVramUsageMb << - " Tex Max: " << textureTotalUsageMb; + " Tex Max: " << textureTotalUsageMb; mFrameDataText = std::unique_ptr(mDefaultFonts.at(1)->buildTextCache(ss.str(), 50.f, 50.f, 0xFF00FFFF)); } @@ -196,12 +220,57 @@ void Window::update(int deltaTime) mTimeSinceLastInput += deltaTime; - if(peekGui()) + if (peekGui()) peekGui()->update(deltaTime); // Update the screensaver if (mScreenSaver) mScreenSaver->update(deltaTime); + + // ============================================================ + // Clock overlay (safe) - update once per second + // Controlled by Settings bool: "ShowClock" + // NOTE: no ThemeData/SystemData access from es-core. + // ============================================================ + const bool showClock = Settings::getInstance()->getBool("ShowClock"); + if (showClock) + { + mClockTimeAccum += deltaTime; + if (mClockTimeAccum >= 1000 || mClockTextCache == nullptr) + { + mClockTimeAccum = 0; + + // Build HH:MM (24h) + std::time_t t = std::time(nullptr); + std::tm tmNow; +#if defined(_WIN32) + localtime_s(&tmNow, &t); +#else + localtime_r(&t, &tmNow); +#endif + std::ostringstream os; + os << std::put_time(&tmNow, "%H:%M"); + const std::string newText = os.str(); + + if (newText != mClockLastText) + { + mClockLastText = newText; + + // Border is ALWAYS black (design decision) + mClockOutlineCache.reset(mDefaultFonts.at(1)->buildTextCache(mClockLastText, 0.f, 0.f, 0x000000FF)); + // Main text (for now white; later can be overridden by style passed from es-app) + mClockTextCache.reset(mDefaultFonts.at(1)->buildTextCache(mClockLastText, 0.f, 0.f, 0xFFFFFFFF)); + } + } + } + else + { + // If disabled, free cache + mClockTimeAccum = 0; + mClockLastText.clear(); + mClockTextCache.reset(); + mClockOutlineCache.reset(); + } } void Window::render() @@ -211,45 +280,76 @@ void Window::render() mRenderedHelpPrompts = false; // draw only bottom and top of GuiStack (if they are different) - if(mGuiStack.size()) + if (mGuiStack.size()) { auto& bottom = mGuiStack.front(); auto& top = mGuiStack.back(); bottom->render(transform); - if(bottom != top) + if (bottom != top) { mBackgroundOverlay->render(transform); top->render(transform); } } - if(!mRenderedHelpPrompts) + if (!mRenderedHelpPrompts) mHelp->render(transform); - if(Settings::getInstance()->getBool("DrawFramerate") && mFrameDataText) + if (Settings::getInstance()->getBool("DrawFramerate") && mFrameDataText) { Renderer::setMatrix(Transform4x4f::Identity()); mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); } unsigned int screensaverTime = (unsigned int)Settings::getInstance()->getInt("ScreenSaverTime"); - if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) + if (mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) startScreenSaver(); // Always call the screensaver render function regardless of whether the screensaver is active // or not because it may perform a fade on transition renderScreenSaver(); - if(mInfoPopup) + // ============================================================ + // Global overlays (ES-DE style) + // - Clock only when ShowClock = true + // - Hide overlays when screensaver active + // ============================================================ + const bool showClock = Settings::getInstance()->getBool("ShowClock"); + if (showClock && !mRenderScreenSaver && mClockTextCache && mClockOutlineCache) + { + // Top-right placement with padding + const float pad = 20.0f; + const float x = Math::round((float)Renderer::getScreenWidth() - mClockTextCache->metrics.size.x() - pad); + const float y = Math::round(pad); + + // 1px outline (4 directions) using the outline cache (black) + static const int off[4][2] = { {-1,0}, {1,0}, {0,-1}, {0,1} }; + for (int i = 0; i < 4; i++) + { + Transform4x4f t = Transform4x4f::Identity(); + t = t.translate(Vector3f(x + (float)off[i][0], y + (float)off[i][1], 0.0f)); + Renderer::setMatrix(t); + mDefaultFonts.at(1)->renderTextCache(mClockOutlineCache.get()); + } + + // Main text + Transform4x4f t = Transform4x4f::Identity(); + t = t.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(t); + mDefaultFonts.at(1)->renderTextCache(mClockTextCache.get()); + } + + // Info popup always on top + if (mInfoPopup) { mInfoPopup->render(transform); } - if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) + if (mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) { unsigned int systemSleepTime = (unsigned int)Settings::getInstance()->getInt("SystemSleepTime"); - if(!isProcessing() && mAllowSleep && systemSleepTime != 0 && mTimeSinceLastInput >= systemSleepTime) { + if (!isProcessing() && mAllowSleep && systemSleepTime != 0 && mTimeSinceLastInput >= systemSleepTime) { mSleeping = true; onSleep(); } @@ -331,29 +431,31 @@ void Window::setHelpPrompts(const std::vector& prompts, const HelpSt std::map inputSeenMap; std::map mappedToSeenMap; - for(auto it = prompts.cbegin(); it != prompts.cend(); it++) + for (auto it = prompts.cbegin(); it != prompts.cend(); it++) { // only add it if the same icon hasn't already been added - if(inputSeenMap.emplace(it->first, true).second) + if (inputSeenMap.emplace(it->first, true).second) { // this symbol hasn't been seen yet, what about the action name? auto mappedTo = mappedToSeenMap.find(it->second); - if(mappedTo != mappedToSeenMap.cend()) + if (mappedTo != mappedToSeenMap.cend()) { // yes, it has! // can we combine? (dpad only) - if((it->first == "up/down" && addPrompts.at(mappedTo->second).first != "left/right") || + if ((it->first == "up/down" && addPrompts.at(mappedTo->second).first != "left/right") || (it->first == "left/right" && addPrompts.at(mappedTo->second).first != "up/down")) { // yes! addPrompts.at(mappedTo->second).first = "up/down/left/right"; // don't need to add this to addPrompts since we just merged - }else{ + } + else { // no, we can't combine! addPrompts.push_back(*it); } - }else{ + } + else { // no, it hasn't! mappedToSeenMap.emplace(it->second, (int)addPrompts.size()); addPrompts.push_back(*it); @@ -376,11 +478,11 @@ void Window::setHelpPrompts(const std::vector& prompts, const HelpSt int i = 0; int aVal = 0; int bVal = 0; - while(map[i] != NULL) + while (map[i] != NULL) { - if(a.first == map[i]) + if (a.first == map[i]) aVal = i; - if(b.first == map[i]) + if (b.first == map[i]) bVal = i; i++; } @@ -391,7 +493,6 @@ void Window::setHelpPrompts(const std::vector& prompts, const HelpSt mHelp->setPrompts(addPrompts); } - void Window::onSleep() { if (Settings::getInstance()->getBool("Windowed")) { @@ -424,7 +525,7 @@ void Window::startScreenSaver(SystemData* system) { Scripting::fireEvent("screensaver-start"); // Tell the GUI components the screensaver is starting - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + for (auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) (*i)->onScreenSaverActivate(); mScreenSaver->startScreenSaver(system); @@ -441,7 +542,7 @@ bool Window::cancelScreenSaver() Scripting::fireEvent("screensaver-stop"); // Tell the GUI components the screensaver has stopped - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + for (auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) (*i)->onScreenSaverDeactivate(); return true; diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 8ab3504c4a..0b0a52f864 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -7,6 +7,7 @@ #include "Settings.h" #include +#include class SystemData; class FileData; @@ -87,8 +88,8 @@ class Window HelpComponent* mHelp; ImageComponent* mBackgroundOverlay; ScreenSaver* mScreenSaver; - InfoPopup* mInfoPopup; - bool mRenderScreenSaver; + InfoPopup* mInfoPopup; + bool mRenderScreenSaver; std::vector mGuiStack; @@ -107,6 +108,14 @@ class Window unsigned int mTimeSinceLastInput; bool mRenderedHelpPrompts; + + // ========================= + // Clock overlay (global) + // ========================= + int mClockTimeAccum; + std::string mClockLastText; + std::unique_ptr mClockTextCache; + std::unique_ptr mClockOutlineCache; }; #endif // ES_CORE_WINDOW_H diff --git a/es-core/src/components/ButtonComponent.cpp b/es-core/src/components/ButtonComponent.cpp index d0f47ce3a7..12c3bf530f 100644 --- a/es-core/src/components/ButtonComponent.cpp +++ b/es-core/src/components/ButtonComponent.cpp @@ -2,118 +2,123 @@ #include "resources/Font.h" #include "utils/StringUtil.h" - -ButtonComponent::ButtonComponent(Window* window, const std::string& text, const std::string& helpText, const std::function& func) : GuiComponent(window), - mBox(window, ":/button.png"), - mFont(Font::get(FONT_SIZE_MEDIUM)), - mFocused(false), - mEnabled(true), - mTextColorFocused(0xFFFFFFFF), mTextColorUnfocused(0x777777FF) +#include "../LocaleESHook.h" // 🔹 Importante para es_translate + +ButtonComponent::ButtonComponent(Window* window, const std::string& text, const std::string& helpText, const std::function& func) + : GuiComponent(window), + mBox(window, ":/button.png"), + mFont(Font::get(FONT_SIZE_MEDIUM)), + mFocused(false), + mEnabled(true), + mTextColorFocused(0xFFFFFFFF), + mTextColorUnfocused(0x777777FF) { - setPressedFunc(func); - setText(text, helpText); - updateImage(); + setPressedFunc(func); + setText(text, helpText); + updateImage(); } void ButtonComponent::onSizeChanged() { - mBox.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); + mBox.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); } void ButtonComponent::setPressedFunc(std::function f) { - mPressedFunc = f; + mPressedFunc = f; } bool ButtonComponent::input(InputConfig* config, Input input) { - if(config->isMappedTo("a", input) && input.value != 0) - { - if(mPressedFunc && mEnabled) - mPressedFunc(); - return true; - } - - return GuiComponent::input(config, input); + if(config->isMappedTo("a", input) && input.value != 0) + { + if(mPressedFunc && mEnabled) + mPressedFunc(); + return true; + } + + return GuiComponent::input(config, input); } void ButtonComponent::setText(const std::string& text, const std::string& helpText) { - mText = Utils::String::toUpper(text); - mHelpText = helpText; + // 🔹 Ahora el texto y helpText pasan por es_translate antes de convertirse a mayúsculas + mText = Utils::String::toUpper(es_translate(text)); + mHelpText = helpText.empty() ? es_translate(text) : es_translate(helpText); - mTextCache = std::unique_ptr(mFont->buildTextCache(mText, 0, 0, getCurTextColor())); + mTextCache = std::unique_ptr(mFont->buildTextCache(mText, 0, 0, getCurTextColor())); - float minWidth = mFont->sizeText("DELETE").x() + 12; - setSize(Math::max(mTextCache->metrics.size.x() + 12, minWidth), mTextCache->metrics.size.y()); + float minWidth = mFont->sizeText("DELETE").x() + 12; + setSize(Math::max(mTextCache->metrics.size.x() + 12, minWidth), mTextCache->metrics.size.y()); - updateHelpPrompts(); + updateHelpPrompts(); } void ButtonComponent::onFocusGained() { - mFocused = true; - updateImage(); + mFocused = true; + updateImage(); } void ButtonComponent::onFocusLost() { - mFocused = false; - updateImage(); + mFocused = false; + updateImage(); } void ButtonComponent::setEnabled(bool enabled) { - mEnabled = enabled; - updateImage(); + mEnabled = enabled; + updateImage(); } void ButtonComponent::updateImage() { - if(!mEnabled || !mPressedFunc) - { - mBox.setImagePath(":/button_filled.png"); - mBox.setCenterColor(0x770000FF); - mBox.setEdgeColor(0x770000FF); - return; - } - - mBox.setCenterColor(0xFFFFFFFF); - mBox.setEdgeColor(0xFFFFFFFF); - mBox.setImagePath(mFocused ? ":/button_filled.png" : ":/button.png"); + if(!mEnabled || !mPressedFunc) + { + mBox.setImagePath(":/button_filled.png"); + mBox.setCenterColor(0x770000FF); + mBox.setEdgeColor(0x770000FF); + return; + } + + mBox.setCenterColor(0xFFFFFFFF); + mBox.setEdgeColor(0xFFFFFFFF); + mBox.setImagePath(mFocused ? ":/button_filled.png" : ":/button.png"); } void ButtonComponent::render(const Transform4x4f& parentTrans) { - Transform4x4f trans = parentTrans * getTransform(); + Transform4x4f trans = parentTrans * getTransform(); - mBox.render(trans); + mBox.render(trans); - if(mTextCache) - { - Vector3f centerOffset((mSize.x() - mTextCache->metrics.size.x()) / 2, (mSize.y() - mTextCache->metrics.size.y()) / 2, 0); - trans = trans.translate(centerOffset); + if(mTextCache) + { + Vector3f centerOffset((mSize.x() - mTextCache->metrics.size.x()) / 2, + (mSize.y() - mTextCache->metrics.size.y()) / 2, 0); + trans = trans.translate(centerOffset); - Renderer::setMatrix(trans); - mTextCache->setColor(getCurTextColor()); - mFont->renderTextCache(mTextCache.get()); - trans = trans.translate(-centerOffset); - } + Renderer::setMatrix(trans); + mTextCache->setColor(getCurTextColor()); + mFont->renderTextCache(mTextCache.get()); + trans = trans.translate(-centerOffset); + } - renderChildren(trans); + renderChildren(trans); } unsigned int ButtonComponent::getCurTextColor() const { - if(!mFocused) - return mTextColorUnfocused; - else - return mTextColorFocused; + return mFocused ? mTextColorFocused : mTextColorUnfocused; } std::vector ButtonComponent::getHelpPrompts() { - std::vector prompts; - prompts.push_back(HelpPrompt("a", mHelpText.empty() ? mText.c_str() : mHelpText.c_str())); - return prompts; + std::vector prompts; + + std::string helpLabel = mHelpText.empty() ? mText : mHelpText; + prompts.push_back(HelpPrompt("a", Utils::String::toUpper(helpLabel))); + + return prompts; } diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index 1ae1c67562..0b5e253341 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -1,337 +1,335 @@ +// es-core/src/components/ComponentList.cpp + #include "components/ComponentList.h" #define TOTAL_HORIZONTAL_PADDING_PX 20 -ComponentList::ComponentList(Window* window) : IList(window, LIST_SCROLL_STYLE_SLOW, LIST_NEVER_LOOP) +ComponentList::ComponentList(Window* window) + : IList(window, LIST_SCROLL_STYLE_SLOW, LIST_NEVER_LOOP) { - mSelectorBarOffset = 0; - mCameraOffset = 0; - mFocused = false; + mSelectorBarOffset = 0; + mCameraOffset = 0; + mFocused = false; } void ComponentList::addRow(const ComponentListRow& row, bool setCursorHere) { - IList::Entry e; - e.name = ""; - e.object = NULL; - e.data = row; + IList::Entry e; + e.name = ""; + e.object = NULL; + e.data = row; - this->add(e); + this->add(e); - for(auto it = mEntries.back().data.elements.cbegin(); it != mEntries.back().data.elements.cend(); it++) - addChild(it->component.get()); + for (auto it = mEntries.back().data.elements.cbegin(); it != mEntries.back().data.elements.cend(); it++) + addChild(it->component.get()); - updateElementSize(mEntries.back().data); - updateElementPosition(mEntries.back().data); + updateElementSize(mEntries.back().data); + updateElementPosition(mEntries.back().data); - if(setCursorHere) - { - mCursor = (int)mEntries.size() - 1; - onCursorChanged(CURSOR_STOPPED); - } + if (setCursorHere) + { + mCursor = (int)mEntries.size() - 1; + onCursorChanged(CURSOR_STOPPED); + } } void ComponentList::onSizeChanged() { - for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) - { - updateElementSize(it->data); - updateElementPosition(it->data); - } + for (auto it = mEntries.cbegin(); it != mEntries.cend(); it++) + { + updateElementSize(it->data); + updateElementPosition(it->data); + } - updateCameraOffset(); + updateCameraOffset(); } void ComponentList::onFocusLost() { - mFocused = false; + mFocused = false; } void ComponentList::onFocusGained() { - mFocused = true; + mFocused = true; } bool ComponentList::input(InputConfig* config, Input input) { - if(size() == 0) - return false; - - // give it to the current row's input handler - if(mEntries.at(mCursor).data.input_handler) - { - if(mEntries.at(mCursor).data.input_handler(config, input)) - return true; - }else{ - // no input handler assigned, do the default, which is to give it to the rightmost element in the row - auto& row = mEntries.at(mCursor).data; - if(row.elements.size()) - { - if(row.elements.back().component->input(config, input)) - return true; - } - } - - // input handler didn't consume the input - try to scroll - if(config->isMappedLike("up", input)) - { - return listInput(input.value != 0 ? -1 : 0); - }else if(config->isMappedLike("down", input)) - { - return listInput(input.value != 0 ? 1 : 0); - - }else if(config->isMappedLike("leftshoulder", input)) - { - return listInput(input.value != 0 ? -6 : 0); - }else if(config->isMappedLike("rightshoulder", input)){ - return listInput(input.value != 0 ? 6 : 0); - } - - return false; + if (size() == 0) + return false; + + // give it to the current row's input handler + if (mEntries.at(mCursor).data.input_handler) + { + if (mEntries.at(mCursor).data.input_handler(config, input)) + return true; + } + else + { + // no input handler assigned, do the default + auto& row = mEntries.at(mCursor).data; + if (row.elements.size()) + { + if (row.elements.back().component->input(config, input)) + return true; + } + } + + // input handler didn't consume input - try to scroll + if (config->isMappedLike("up", input)) + { + return listInput(input.value != 0 ? -1 : 0); + } + else if (config->isMappedLike("down", input)) + { + return listInput(input.value != 0 ? 1 : 0); + } + else if (config->isMappedLike("leftshoulder", input)) + { + return listInput(input.value != 0 ? -6 : 0); + } + else if (config->isMappedLike("rightshoulder", input)) + { + return listInput(input.value != 0 ? 6 : 0); + } + + return false; } void ComponentList::update(int deltaTime) { - listUpdate(deltaTime); - - if(size()) - { - // update our currently selected row - for(auto it = mEntries.at(mCursor).data.elements.cbegin(); it != mEntries.at(mCursor).data.elements.cend(); it++) - it->component->update(deltaTime); - } + listUpdate(deltaTime); + + if (size()) + { + for (auto it = mEntries.at(mCursor).data.elements.cbegin(); it != mEntries.at(mCursor).data.elements.cend(); it++) + it->component->update(deltaTime); + } } void ComponentList::onCursorChanged(const CursorState& state) { - // update the selector bar position - // in the future this might be animated - mSelectorBarOffset = 0; - for(int i = 0; i < mCursor; i++) - { - mSelectorBarOffset += getRowHeight(mEntries.at(i).data); - } + mSelectorBarOffset = 0; + for (int i = 0; i < mCursor; i++) + { + mSelectorBarOffset += getRowHeight(mEntries.at(i).data); + } - updateCameraOffset(); + updateCameraOffset(); - // this is terribly inefficient but we don't know what we came from so... - if(size()) - { - for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) - it->data.elements.back().component->onFocusLost(); + if (size()) + { + for (auto it = mEntries.cbegin(); it != mEntries.cend(); it++) + it->data.elements.back().component->onFocusLost(); - mEntries.at(mCursor).data.elements.back().component->onFocusGained(); - } + mEntries.at(mCursor).data.elements.back().component->onFocusGained(); + } - if(mCursorChangedCallback) - mCursorChangedCallback(state); + if (mCursorChangedCallback) + mCursorChangedCallback(state); - updateHelpPrompts(); + updateHelpPrompts(); } void ComponentList::updateCameraOffset() { - // move the camera to scroll - const float totalHeight = getTotalRowHeight(); - if(totalHeight > mSize.y()) - { - float target = mSelectorBarOffset + getRowHeight(mEntries.at(mCursor).data)/2 - (mSize.y() / 2); - - // clamp it - mCameraOffset = 0; - unsigned int i = 0; - while(mCameraOffset < target && i < mEntries.size()) - { - mCameraOffset += getRowHeight(mEntries.at(i).data); - i++; - } - - if(mCameraOffset < 0) - mCameraOffset = 0; - else if(mCameraOffset + mSize.y() > totalHeight) - mCameraOffset = totalHeight - mSize.y(); - }else{ - mCameraOffset = 0; - } + const float totalHeight = getTotalRowHeight(); + if (totalHeight > mSize.y()) + { + float target = mSelectorBarOffset + getRowHeight(mEntries.at(mCursor).data) / 2 - (mSize.y() / 2); + + mCameraOffset = 0; + unsigned int i = 0; + while (mCameraOffset < target && i < mEntries.size()) + { + mCameraOffset += getRowHeight(mEntries.at(i).data); + i++; + } + + if (mCameraOffset < 0) + mCameraOffset = 0; + else if (mCameraOffset + mSize.y() > totalHeight) + mCameraOffset = totalHeight - mSize.y(); + } + else + { + mCameraOffset = 0; + } } void ComponentList::render(const Transform4x4f& parentTrans) { - if(!size()) - return; - - Transform4x4f trans = parentTrans * getTransform(); - - // clip everything to be inside our bounds - Vector3f dim(mSize.x(), mSize.y(), 0); - dim = trans * dim - trans.translation(); - Renderer::pushClipRect(Vector2i((int)trans.translation().x(), (int)trans.translation().y()), - Vector2i((int)Math::round(dim.x()), (int)Math::round(dim.y() + 1))); - - // scroll the camera - trans.translate(Vector3f(0, -Math::round(mCameraOffset), 0)); - - // draw our entries - std::vector drawAfterCursor; - bool drawAll; - for(unsigned int i = 0; i < mEntries.size(); i++) - { - auto& entry = mEntries.at(i); - drawAll = !mFocused || i != (unsigned int)mCursor; - for(auto it = entry.data.elements.cbegin(); it != entry.data.elements.cend(); it++) - { - if(drawAll || it->invert_when_selected) - { - it->component->render(trans); - }else{ - drawAfterCursor.push_back(it->component.get()); - } - } - } - - // custom rendering - Renderer::setMatrix(trans); - - // draw selector bar - if(mFocused) - { - // inversion: src * (1 - dst) + dst * 0 = where src = 1 - // need a function that goes roughly 0x777777 -> 0xFFFFFF - // and 0xFFFFFF -> 0x777777 - // (1 - dst) + 0x77 - - const float selectedRowHeight = getRowHeight(mEntries.at(mCursor).data); - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0xFFFFFFFF, 0xFFFFFFFF, false, Renderer::Blend::ONE_MINUS_DST_COLOR, Renderer::Blend::ZERO); - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0x777777FF, 0x777777FF, false, Renderer::Blend::ONE, Renderer::Blend::ONE); - - // hack to draw 2px dark on left/right of the bar - Renderer::drawRect(0.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF, 0x878787FF); - Renderer::drawRect(mSize.x() - 2.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF, 0x878787FF); - - for(auto it = drawAfterCursor.cbegin(); it != drawAfterCursor.cend(); it++) - (*it)->render(trans); - - // reset matrix if one of these components changed it - if(drawAfterCursor.size()) - Renderer::setMatrix(trans); - } - - // draw separators - float y = 0; - for(unsigned int i = 0; i < mEntries.size(); i++) - { - Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, 0xC6C7C6FF, 0xC6C7C6FF); - y += getRowHeight(mEntries.at(i).data); - } - Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, 0xC6C7C6FF, 0xC6C7C6FF); - - Renderer::popClipRect(); + if (!size()) + return; + + Transform4x4f trans = parentTrans * getTransform(); + + Vector3f dim(mSize.x(), mSize.y(), 0); + dim = trans * dim - trans.translation(); + Renderer::pushClipRect( + Vector2i((int)trans.translation().x(), (int)trans.translation().y()), + Vector2i((int)Math::round(dim.x()), (int)Math::round(dim.y() + 1))); + + trans.translate(Vector3f(0, -Math::round(mCameraOffset), 0)); + + // draw entries + std::vector drawAfterCursor; + bool drawAll; + + for (unsigned int i = 0; i < mEntries.size(); i++) + { + auto& entry = mEntries.at(i); + drawAll = !mFocused || i != (unsigned int)mCursor; + + for (auto it = entry.data.elements.cbegin(); it != entry.data.elements.cend(); it++) + { + if (drawAll || it->invert_when_selected) + it->component->render(trans); + else + drawAfterCursor.push_back(it->component.get()); + } + } + + Renderer::setMatrix(trans); + + // draw selector bar + if (mFocused) + { + const float selectedRowHeight = getRowHeight(mEntries.at(mCursor).data); + + // --- BARRA AZUL MÁS TRANSPARENTE (30%) --- +const unsigned int barColor = 0x0063BF4C; // azul PS4 con ~30% de opacidad +const unsigned int borderColor = 0x00336666; // bordes sutiles ~40% + + // barra + Renderer::drawRect( + 0.0f, + mSelectorBarOffset, + mSize.x(), + selectedRowHeight, + barColor, + barColor); + + // bordes laterales + Renderer::drawRect(0.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, borderColor, borderColor); + Renderer::drawRect(mSize.x() - 2.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, borderColor, borderColor); + + // texto → SIEMPRE encima + for (auto it = drawAfterCursor.cbegin(); it != drawAfterCursor.cend(); it++) + (*it)->render(trans); + + if (drawAfterCursor.size()) + Renderer::setMatrix(trans); + } + + // draw separators + float y = 0; + for (unsigned int i = 0; i < mEntries.size(); i++) + { + Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, 0xC6C7C6FF, 0xC6C7C6FF); + y += getRowHeight(mEntries.at(i).data); + } + Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, 0xC6C7C6FF, 0xC6C7C6FF); + + Renderer::popClipRect(); } float ComponentList::getRowHeight(const ComponentListRow& row) const { - // returns the highest component height found in the row - float height = 0; - for(unsigned int i = 0; i < row.elements.size(); i++) - { - if(row.elements.at(i).component->getSize().y() > height) - height = row.elements.at(i).component->getSize().y(); - } - - return height; + float height = 0; + for (unsigned int i = 0; i < row.elements.size(); i++) + { + if (row.elements.at(i).component->getSize().y() > height) + height = row.elements.at(i).component->getSize().y(); + } + + return height; } float ComponentList::getTotalRowHeight() const { - float height = 0; - for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) - { - height += getRowHeight(it->data); - } + float height = 0; + for (auto it = mEntries.cbegin(); it != mEntries.cend(); it++) + height += getRowHeight(it->data); - return height; + return height; } void ComponentList::updateElementPosition(const ComponentListRow& row) { - float yOffset = 0; - for(auto it = mEntries.cbegin(); it != mEntries.cend() && &it->data != &row; it++) - { - yOffset += getRowHeight(it->data); - } - - // assumes updateElementSize has already been called - float rowHeight = getRowHeight(row); - - float x = TOTAL_HORIZONTAL_PADDING_PX / 2; - for(unsigned int i = 0; i < row.elements.size(); i++) - { - const auto comp = row.elements.at(i).component; - - // center vertically - comp->setPosition(x, (rowHeight - comp->getSize().y()) / 2 + yOffset); - x += comp->getSize().x(); - } + float yOffset = 0; + for (auto it = mEntries.cbegin(); it != mEntries.cend() && &it->data != &row; it++) + yOffset += getRowHeight(it->data); + + float rowHeight = getRowHeight(row); + float x = TOTAL_HORIZONTAL_PADDING_PX / 2; + + for (unsigned int i = 0; i < row.elements.size(); i++) + { + const auto comp = row.elements.at(i).component; + + comp->setPosition(x, (rowHeight - comp->getSize().y()) / 2 + yOffset); + x += comp->getSize().x(); + } } void ComponentList::updateElementSize(const ComponentListRow& row) { - float width = mSize.x() - TOTAL_HORIZONTAL_PADDING_PX; - std::vector< std::shared_ptr > resizeVec; - - for(auto it = row.elements.cbegin(); it != row.elements.cend(); it++) - { - if(it->resize_width) - resizeVec.push_back(it->component); - else - width -= it->component->getSize().x(); - } - - // redistribute the "unused" width equally among the components with resize_width set to true - width = width / resizeVec.size(); - for(auto it = resizeVec.cbegin(); it != resizeVec.cend(); it++) - { - (*it)->setSize(width, (*it)->getSize().y()); - } + float width = mSize.x() - TOTAL_HORIZONTAL_PADDING_PX; + std::vector> resizeVec; + + for (auto it = row.elements.cbegin(); it != row.elements.cend(); it++) + { + if (it->resize_width) + resizeVec.push_back(it->component); + else + width -= it->component->getSize().x(); + } + + width = width / resizeVec.size(); + for (auto it = resizeVec.cbegin(); it != resizeVec.cend(); it++) + (*it)->setSize(width, (*it)->getSize().y()); } void ComponentList::textInput(const char* text) { - if(!size()) - return; + if (!size()) + return; - mEntries.at(mCursor).data.elements.back().component->textInput(text); + mEntries.at(mCursor).data.elements.back().component->textInput(text); } std::vector ComponentList::getHelpPrompts() { - if(!size()) - return std::vector(); - - std::vector prompts = mEntries.at(mCursor).data.elements.back().component->getHelpPrompts(); - - if(size() > 1) - { - bool addMovePrompt = true; - for(auto it = prompts.cbegin(); it != prompts.cend(); it++) - { - if(it->first == "up/down" || it->first == "up/down/left/right") - { - addMovePrompt = false; - break; - } - } - - if(addMovePrompt) - prompts.push_back(HelpPrompt("up/down", "choose")); - } - - return prompts; + if (!size()) + return std::vector(); + + std::vector prompts = mEntries.at(mCursor).data.elements.back().component->getHelpPrompts(); + + if (size() > 1) + { + bool addMovePrompt = true; + for (auto it = prompts.cbegin(); it != prompts.cend(); it++) + { + if (it->first == "up/down" || it->first == "up/down/left/right") + { + addMovePrompt = false; + break; + } + } + + if (addMovePrompt) + prompts.push_back(HelpPrompt("up/down", "choose")); + } + + return prompts; } bool ComponentList::moveCursor(int amt) { - bool ret = listInput(amt); - listInput(0); - return ret; + bool ret = listInput(amt); + listInput(0); + return ret; } diff --git a/es-core/src/components/ControllerStatusComponent.cpp b/es-core/src/components/ControllerStatusComponent.cpp new file mode 100644 index 0000000000..eae3b4953c --- /dev/null +++ b/es-core/src/components/ControllerStatusComponent.cpp @@ -0,0 +1,89 @@ +#include "components/ControllerStatusComponent.h" + +#include "InputManager.h" +#include "renderers/Renderer.h" +#include "resources/Font.h" + +ControllerStatusComponent::ControllerStatusComponent(Window* window) + : GuiComponent(window), + mMaxControllers(4), + mIconSize(0.035f) +{ + // Fuente players.ttf dentro de resources + mFont = Font::getFromFile( + ":/fonts/players.ttf", + (int)(Renderer::getScreenHeight() * mIconSize), + FONT_PATH_REGULAR); + + setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); +} + +void ControllerStatusComponent::setMaxControllers(int maxControllers) +{ + if (maxControllers < 1) + maxControllers = 1; + mMaxControllers = maxControllers; +} + +void ControllerStatusComponent::setIconSize(float size) +{ + if (size < 0.01f) + size = 0.01f; + + mIconSize = size; + + // Recarga tamaño de fuente + mFont = Font::getFromFile( + ":/fonts/players.ttf", + (int)(Renderer::getScreenHeight() * mIconSize), + FONT_PATH_REGULAR); +} + +void ControllerStatusComponent::update(int /*deltaTime*/) +{ + // UI pasiva: nada por ahora +} + +void ControllerStatusComponent::render(const Transform4x4f& parentTrans) +{ + if (!mFont) + return; + + // Batocera/ES-DE style: tomamos el mapa real de “players” + // En tu log ya se ve que InputManager detecta joysticks. + auto players = InputManager::getInstance()->lastKnownPlayersDeviceIndexes(); + + int count = 0; + for (auto& kv : players) + { + if (kv.second >= 0) + count++; + } + + if (count <= 0) + return; + + if (count > mMaxControllers) + count = mMaxControllers; + + Transform4x4f trans = parentTrans * getTransform(); + + // Arriba a la derecha (ajustable después por tema si querés) + const float y = Renderer::getScreenHeight() * 0.04f; + + // Render de “P1..Pn” como dígitos (players.ttf en Batocera suele mapear 1..4) + // Vamos de derecha a izquierda para que quede pegado al borde. + float x = Renderer::getScreenWidth() * 0.96f; + + for (int i = 0; i < count; i++) + { + const std::string glyph = std::to_string(i + 1); + const auto size = mFont->sizeText(glyph); + + x -= size.x(); + mFont->renderText(glyph, x, y, trans, 0xFFFFFFFF); + + // pequeño espaciado + x -= Renderer::getScreenWidth() * 0.006f; + } +} diff --git a/es-core/src/components/ControllerStatusComponent.h b/es-core/src/components/ControllerStatusComponent.h new file mode 100644 index 0000000000..606a518e83 --- /dev/null +++ b/es-core/src/components/ControllerStatusComponent.h @@ -0,0 +1,24 @@ +#pragma once + +#include "GuiComponent.h" +#include +#include + +class Font; + +class ControllerStatusComponent : public GuiComponent +{ +public: + explicit ControllerStatusComponent(Window* window); + + void setMaxControllers(int maxControllers); + void setIconSize(float size); // relativo a altura (ej: 0.035f) + + void update(int deltaTime) override; + void render(const Transform4x4f& parentTrans) override; + +private: + std::shared_ptr mFont; + int mMaxControllers; + float mIconSize; +}; diff --git a/es-core/src/components/DateTimeComponent.cpp b/es-core/src/components/DateTimeComponent.cpp index c1e719b93c..2eafb29c18 100644 --- a/es-core/src/components/DateTimeComponent.cpp +++ b/es-core/src/components/DateTimeComponent.cpp @@ -3,6 +3,7 @@ #include "utils/StringUtil.h" #include "Log.h" #include "Settings.h" +#include "../LocaleESHook.h" // <-- IMPORTANTE: hook de traducción #include @@ -49,34 +50,51 @@ void DateTimeComponent::onTextChanged() std::string DateTimeComponent::getDisplayString() const { - if(std::difftime(mTime.getTime(), Utils::Time::BLANK_DATE) == 0.0) { + if (std::difftime(mTime.getTime(), Utils::Time::BLANK_DATE) == 0.0) return ""; - } - if (mDisplayRelative) { - //relative time - if(mTime.getTime() == Utils::Time::NOT_A_DATE_TIME) - return "never"; + // Modo relativo (ej.: "123 días atrás") + if (mDisplayRelative) + { + if (mTime.getTime() == Utils::Time::NOT_A_DATE_TIME) + return es_translate("NEVER"); Utils::Time::DateTime now(Utils::Time::now()); Utils::Time::Duration dur(now.getTime() - mTime.getTime()); char buf[64]; - if(dur.getDays() > 0) - sprintf(buf, "%d day%s ago", dur.getDays(), (dur.getDays() > 1) ? "s" : ""); - else if(dur.getHours() > 0) - sprintf(buf, "%d hour%s ago", dur.getHours(), (dur.getHours() > 1) ? "s" : ""); - else if(dur.getMinutes() > 0) - sprintf(buf, "%d minute%s ago", dur.getMinutes(), (dur.getMinutes() > 1) ? "s" : ""); + if (dur.getDays() > 0) + { + snprintf(buf, sizeof(buf), "%d %s", + dur.getDays(), + es_translate("DAYS AGO").c_str()); + } + else if (dur.getHours() > 0) + { + snprintf(buf, sizeof(buf), "%d %s", + dur.getHours(), + es_translate("HOURS AGO").c_str()); + } + else if (dur.getMinutes() > 0) + { + snprintf(buf, sizeof(buf), "%d %s", + dur.getMinutes(), + es_translate("MINUTES AGO").c_str()); + } else - sprintf(buf, "%d second%s ago", dur.getSeconds(), (dur.getSeconds() > 1) ? "s" : ""); + { + snprintf(buf, sizeof(buf), "%d %s", + dur.getSeconds(), + es_translate("SECONDS AGO").c_str()); + } return std::string(buf); } - if(mTime.getTime() == Utils::Time::NOT_A_DATE_TIME) - return "unknown"; + // Modo fecha normal + if (mTime.getTime() == Utils::Time::NOT_A_DATE_TIME) + return es_translate("UNKNOWN"); return Utils::Time::timeToString(mTime.getTime(), mFormat); } @@ -94,13 +112,13 @@ void DateTimeComponent::applyTheme(const std::shared_ptr& theme, cons using namespace ThemeFlags; const ThemeData::ThemeElement* elem = theme->getElement(view, element, "datetime"); - if(!elem) + if (!elem) return; - if(elem->has("displayRelative")) + if (elem->has("displayRelative")) setDisplayRelative(elem->get("displayRelative")); - if(elem->has("format")) + if (elem->has("format")) setFormat(elem->get("format")); if (properties & COLOR && elem->has("color")) @@ -112,23 +130,23 @@ void DateTimeComponent::applyTheme(const std::shared_ptr& theme, cons setRenderBackground(true); } - if(properties & ALIGNMENT && elem->has("alignment")) + if (properties & ALIGNMENT && elem->has("alignment")) { std::string str = elem->get("alignment"); - if(str == "left") + if (str == "left") setHorizontalAlignment(ALIGN_LEFT); - else if(str == "center") + else if (str == "center") setHorizontalAlignment(ALIGN_CENTER); - else if(str == "right") + else if (str == "right") setHorizontalAlignment(ALIGN_RIGHT); else - LOG(LogError) << "Unknown text alignment string: " << str; + LOG(LogError) << "Unknown text alignment string: " << str; } - if(properties & FORCE_UPPERCASE && elem->has("forceUppercase")) + if (properties & FORCE_UPPERCASE && elem->has("forceUppercase")) setUppercase(elem->get("forceUppercase")); - if(properties & LINE_SPACING && elem->has("lineSpacing")) + if (properties & LINE_SPACING && elem->has("lineSpacing")) setLineSpacing(elem->get("lineSpacing")); setFont(Font::getFromTheme(elem, properties, mFont)); diff --git a/es-core/src/components/HelpComponent.cpp b/es-core/src/components/HelpComponent.cpp index 1d7ef55291..0030f7b8c8 100644 --- a/es-core/src/components/HelpComponent.cpp +++ b/es-core/src/components/HelpComponent.cpp @@ -7,13 +7,15 @@ #include "utils/StringUtil.h" #include "Log.h" #include "Settings.h" +#include "../LocaleESHook.h" // Para es_translate() -#define OFFSET_X 12 // move the entire thing right by this amount (px) -#define OFFSET_Y 12 // move the entire thing up by this amount (px) +#define OFFSET_X 12 +#define OFFSET_Y 12 -#define ICON_TEXT_SPACING 8 // space between [icon] and [text] (px) -#define ENTRY_SPACING 16 // space between [text] and next [icon] (px) +#define ICON_TEXT_SPACING 8 +#define ENTRY_SPACING 16 +// 🔹 Mapa de íconos static const std::map ICON_PATH_MAP { { "up/down", ":/help/dpad_updown.svg" }, { "left/right", ":/help/dpad_leftright.svg" }, @@ -51,6 +53,32 @@ void HelpComponent::setStyle(const HelpStyle& style) updateGrid(); } +// 🔹 Normaliza claves como "favorites", "favs", "favorite" → "FAVORITES" +static std::string normalizeKey(const std::string& key) +{ + std::string k = Utils::String::toLower(key); + + if (k == "favorites" || k == "favs" || k == "favorite") + return "FAVORITES"; + + if (k == "back") + return "BACK"; + + if (k == "select") + return "SELECT"; + + if (k == "menu") + return "MENU"; + + if (k == "random") + return "RANDOM"; + + if (k == "choose") + return "CHOOSE"; + + return Utils::String::toUpper(key); +} + void HelpComponent::updateGrid() { if(!Settings::getInstance()->getBool("ShowHelpPrompts") || mPrompts.empty()) @@ -60,15 +88,14 @@ void HelpComponent::updateGrid() } std::shared_ptr& font = mStyle.font; - mGrid = std::make_shared(mWindow, Vector2i((int)mPrompts.size() * 4, 1)); - // [icon] [spacer1] [text] [spacer2] std::vector< std::shared_ptr > icons; std::vector< std::shared_ptr > labels; float width = 0; const float height = Math::round(font->getLetterHeight() * 1.25f); + for(auto it = mPrompts.cbegin(); it != mPrompts.cend(); it++) { auto icon = std::make_shared(mWindow); @@ -77,16 +104,26 @@ void HelpComponent::updateGrid() icon->setResize(0, height); icons.push_back(icon); - auto lbl = std::make_shared(mWindow, Utils::String::toUpper(it->second), font, mStyle.textColor); + // 🔹 Traducción corregida + std::string normalizedKey = normalizeKey(it->second); + std::string translated = es_translate(normalizedKey); + + auto lbl = std::make_shared( + mWindow, + Utils::String::toUpper(translated), + font, + mStyle.textColor + ); labels.push_back(lbl); width += icon->getSize().x() + lbl->getSize().x() + ICON_TEXT_SPACING + ENTRY_SPACING; } mGrid->setSize(width, height); + for(unsigned int i = 0; i < icons.size(); i++) { - const int col = i*4; + const int col = i * 4; mGrid->setColWidthPerc(col, icons.at(i)->getSize().x() / width); mGrid->setColWidthPerc(col + 1, ICON_TEXT_SPACING / width); mGrid->setColWidthPerc(col + 2, labels.at(i)->getSize().x() / width); @@ -96,8 +133,6 @@ void HelpComponent::updateGrid() } mGrid->setPosition(Vector3f(mStyle.position.x(), mStyle.position.y(), 0.0f)); - //mGrid->setPosition(OFFSET_X, Renderer::getScreenHeight() - mGrid->getSize().y() - OFFSET_Y); - mGrid->setOrigin(mStyle.origin); } std::shared_ptr HelpComponent::getIconTexture(const char* name) @@ -114,11 +149,11 @@ std::shared_ptr HelpComponent::getIconTexture(const char* name) } if(!ResourceManager::getInstance()->fileExists(pathLookup->second)) { - LOG(LogError) << "Help icon \"" << name << "\" - corresponding image file \"" << pathLookup->second << "\" misisng!"; + LOG(LogError) << "Missing icon file \"" << pathLookup->second << "\"!"; return nullptr; } - std::shared_ptr tex = TextureResource::get(pathLookup->second); + auto tex = TextureResource::get(pathLookup->second); mIconCache[std::string(name)] = tex; return tex; } @@ -128,9 +163,7 @@ void HelpComponent::setOpacity(unsigned char opacity) GuiComponent::setOpacity(opacity); for(unsigned int i = 0; i < mGrid->getChildCount(); i++) - { mGrid->getChild(i)->setOpacity(opacity); - } } void HelpComponent::render(const Transform4x4f& parentTrans) diff --git a/es-core/src/components/MarqueeTextComponent.cpp b/es-core/src/components/MarqueeTextComponent.cpp new file mode 100644 index 0000000000..340cecb55f --- /dev/null +++ b/es-core/src/components/MarqueeTextComponent.cpp @@ -0,0 +1,104 @@ +#include "components/MarqueeTextComponent.h" + +#include "renderers/Renderer.h" +#include "Window.h" + +MarqueeTextComponent::MarqueeTextComponent( + Window* window, + const std::string& text, + const std::shared_ptr& font, + unsigned int color, + Alignment align) + : TextComponent(window, text, font, color, align), + mScrollEnabled(true), + mScrollingActive(false), + mScrollSpeed(40.0f), // velocidad base + mScrollGap(40.0f), // gap al final + mScrollOffset(0.0f), + mScrollDelayMs(1200), // 1.2s de espera inicial + mElapsedMs(0) +{ +} + +void MarqueeTextComponent::setText(const std::string& text) +{ + // llamamos al TextComponent original + TextComponent::setText(text); + + // reiniciamos el estado del scroll + mScrollOffset = 0.0f; + mElapsedMs = 0; + mScrollingActive = false; +} + +void MarqueeTextComponent::update(int deltaTime) +{ + // Primero dejamos que TextComponent haga lo suyo (cacheo, etc.) + TextComponent::update(deltaTime); + + if (!mScrollEnabled) + return; + + // Acumulamos tiempo + mElapsedMs += deltaTime; + + // Activamos scroll después del delay inicial + if (!mScrollingActive) + { + if (mElapsedMs >= mScrollDelayMs) + { + mScrollingActive = true; + mElapsedMs = 0; + } + else + { + return; + } + } + + // deltaTime viene en ms → pasamos a segundos + const float dt = deltaTime / 1000.0f; + mScrollOffset += mScrollSpeed * dt; + + // Usamos un ciclo simple basado en el ancho del componente + gap fijo + const float boxWidth = getSize().x(); + const float resetPoint = boxWidth + mScrollGap; + + if (boxWidth > 0.0f && mScrollOffset > resetPoint) + { + mScrollOffset = 0.0f; + mElapsedMs = 0; + mScrollingActive = false; // volvemos a aplicar delay antes del próximo ciclo + } +} + +void MarqueeTextComponent::render(const Transform4x4f& parentTrans) +{ + if (!isVisible()) + return; + + // Transform base del componente (sin scroll) + Transform4x4f trans = parentTrans * getTransform(); + Vector3f pos = trans.translation(); + Vector2f size = getSize(); + + // Definimos el área de recorte igual que otros componentes + Renderer::pushClipRect( + Vector2i((int)pos.x(), (int)pos.y()), + Vector2i((int)size.x(), (int)size.y())); + + // Creamos un parentTrans con el offset de scroll aplicado + Transform4x4f parentWithScroll = parentTrans; + + if (mScrollingActive) + { + // movemos el texto hacia la izquierda dentro del cuadro + parentWithScroll.translate(Vector3f(-mScrollOffset, 0.0f, 0.0f)); + } + + // Dejamos que TextComponent haga el render normal, + // pero usando el parentTrans desplazado + TextComponent::render(parentWithScroll); + + Renderer::popClipRect(); +} diff --git a/es-core/src/components/MarqueeTextComponent.h b/es-core/src/components/MarqueeTextComponent.h new file mode 100644 index 0000000000..f4fa0465f5 --- /dev/null +++ b/es-core/src/components/MarqueeTextComponent.h @@ -0,0 +1,56 @@ +#pragma once + +#include "components/TextComponent.h" + +// Componente de texto con scroll horizontal sencillo. +// No rompe compatibilidad con temas existentes y usa la misma API básica de TextComponent. +// +// NOTA: Esta versión no intenta calcular el ancho real del texto ni tocar mTextCache. +// Solo desplaza el texto hacia la izquierda dentro del área del componente, +// con un delay inicial y un bucle simple. +// + +class MarqueeTextComponent : public TextComponent +{ +public: + MarqueeTextComponent( + Window* window, + const std::string& text, + const std::shared_ptr& font, + unsigned int color, + Alignment align = ALIGN_LEFT); + + // Redefinimos setText solo para reiniciar el estado interno del scroll. + // No usamos 'override' porque en tu TextComponent setText NO es virtual. + void setText(const std::string& text); + + // Activar / desactivar scroll + void setScrollEnabled(bool enabled) { mScrollEnabled = enabled; } + bool getScrollEnabled() const { return mScrollEnabled; } + + // Velocidad en píxeles por segundo (por defecto 40) + void setScrollSpeed(float speed) { mScrollSpeed = speed; } + float getScrollSpeed() const { return mScrollSpeed; } + + // Retraso antes de empezar a moverse (en ms) + void setScrollDelay(int delayMs) { mScrollDelayMs = delayMs; } + int getScrollDelay() const { return mScrollDelayMs; } + + // Espacio "virtual" después del texto antes de reiniciar (en píxeles) + void setScrollGap(float gap) { mScrollGap = gap; } + float getScrollGap() const { return mScrollGap; } + + void update(int deltaTime) override; + void render(const Transform4x4f& parentTrans) override; + +private: + bool mScrollEnabled; // ¿scroll activado? + bool mScrollingActive; // ¿estamos desplazando ahora? + + float mScrollSpeed; // px/seg + float mScrollGap; // gap fijo antes de reiniciar + float mScrollOffset; // desplazamiento actual en px + + int mScrollDelayMs; // delay inicial + int mElapsedMs; // tiempo acumulado desde que se mostró +}; diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 2e60b59459..e347bb32ba 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -1,6 +1,7 @@ #include "components/MenuComponent.h" #include "components/ButtonComponent.h" +#include "Settings.h" #define BUTTON_GRID_VERT_PADDING 32 #define BUTTON_GRID_HORIZ_PADDING 10 @@ -13,12 +14,15 @@ MenuComponent::MenuComponent(Window* window, const char* title, const std::share addChild(&mBackground); addChild(&mGrid); - mBackground.setImagePath(":/frame.png"); + // Modo claro/oscuro para el fondo del menú + bool darkMenu = Settings::getInstance()->getBool("MenuDark"); + mBackground.setImagePath(darkMenu ? ":/frame_dark.png" : ":/frame.png"); // set up title mTitle = std::make_shared(mWindow); mTitle->setHorizontalAlignment(ALIGN_CENTER); - mTitle->setColor(0x555555FF); + // Título blanco en modo oscuro, gris en modo claro + mTitle->setColor(darkMenu ? 0xFFFFFFFF : 0x555555FF); setTitle(title, titleFont); mGrid.setEntry(mTitle, Vector2i(0, 0), false); diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index 7a8f52cdf3..4cd27ad7c7 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -5,295 +5,317 @@ #include "Settings.h" TextComponent::TextComponent(Window* window) : GuiComponent(window), - mFont(Font::get(FONT_SIZE_MEDIUM)), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), - mHorizontalAlignment(ALIGN_LEFT), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), - mRenderBackground(false) + mFont(Font::get(FONT_SIZE_MEDIUM)), mUppercase(false), + mColor(0xFFFFFFFF), mBgColor(0x00000000), + mStrokeColor(0x000000FF), mColorOpacity(0xFF), + mBgColorOpacity(0x00), mStrokeColorOpacity(0xFF), + mRenderBackground(false), + mStrokeSize(0.0f), // *** SIN BORDE POR DEFECTO *** + mAutoCalcExtent(true, true), + mHorizontalAlignment(ALIGN_LEFT), + mVerticalAlignment(ALIGN_CENTER), + mLineSpacing(1.5f) { } -TextComponent::TextComponent(Window* window, const std::string& text, const std::shared_ptr& font, unsigned int color, Alignment align, - Vector3f pos, Vector2f size, unsigned int bgcolor) : GuiComponent(window), - mFont(NULL), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), - mHorizontalAlignment(align), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), - mRenderBackground(false) +TextComponent::TextComponent(Window* window, const std::string& text, const std::shared_ptr& font, + unsigned int color, Alignment align, Vector3f pos, Vector2f size, unsigned int bgcolor) + : GuiComponent(window), + mFont(nullptr), mUppercase(false), mColor(color), mBgColor(bgcolor), + mStrokeColor(0x000000FF), mColorOpacity(color & 0xFF), + mBgColorOpacity(bgcolor & 0xFF), mStrokeColorOpacity(0xFF), + mRenderBackground(bgcolor != 0), // activar si hay fondo + mAutoCalcExtent(true, true), + mHorizontalAlignment(align), mVerticalAlignment(ALIGN_CENTER), + mLineSpacing(1.5f), + mStrokeSize(0.0f) // sin borde { - setFont(font); - setColor(color); - setBackgroundColor(bgcolor); - setText(text); - setPosition(pos); - setSize(size); + setFont(font); + setText(text); + setPosition(pos); + setSize(size); } void TextComponent::onSizeChanged() { - mAutoCalcExtent = Vector2i((getSize().x() == 0), (getSize().y() == 0)); - onTextChanged(); + mAutoCalcExtent = Vector2i((getSize().x() == 0), (getSize().y() == 0)); + onTextChanged(); } void TextComponent::setFont(const std::shared_ptr& font) { - mFont = font; - onTextChanged(); + mFont = font; + onTextChanged(); } -// Set the color of the font/text void TextComponent::setColor(unsigned int color) { - mColor = color; - mColorOpacity = mColor & 0x000000FF; - onColorChanged(); + mColor = color; + mColorOpacity = (color & 0xFF); + onColorChanged(); } -// Set the color of the background box void TextComponent::setBackgroundColor(unsigned int color) { - mBgColor = color; - mBgColorOpacity = mBgColor & 0x000000FF; + mBgColor = color; + mBgColorOpacity = (color & 0xFF); + + // activar fondo automáticamente si el color no es transparente + mRenderBackground = (mBgColorOpacity > 0); } void TextComponent::setRenderBackground(bool render) { - mRenderBackground = render; + mRenderBackground = render; +} + +void TextComponent::setTextStrokeColor(unsigned int color) +{ + mStrokeColor = color; + mStrokeColorOpacity = (color & 0xFF); +} + +void TextComponent::setTextStrokeSize(float size) +{ + mStrokeSize = size; } -// Scale the opacity void TextComponent::setOpacity(unsigned char opacity) { - // This method is mostly called to do fading in-out of the Text component element. - // Therefore, we assume here that opacity is a fractional value (expressed as an int 0-255), - // of the opacity originally set with setColor() or setBackgroundColor(). + unsigned char o = (unsigned char)((float)opacity / 255.f * (float)mColorOpacity); + mColor = (mColor & 0xFFFFFF00) | o; - unsigned char o = (unsigned char)((float)opacity / 255.f * (float) mColorOpacity); - mColor = (mColor & 0xFFFFFF00) | (unsigned char) o; + unsigned char bgo = (unsigned char)((float)opacity / 255.f * (float)mBgColorOpacity); + mBgColor = (mBgColor & 0xFFFFFF00) | bgo; - unsigned char bgo = (unsigned char)((float)opacity / 255.f * (float)mBgColorOpacity); - mBgColor = (mBgColor & 0xFFFFFF00) | (unsigned char)bgo; + unsigned char so = (unsigned char)((float)opacity / 255.f * (float)mStrokeColorOpacity); + mStrokeColor = (mStrokeColor & 0xFFFFFF00) | so; - onColorChanged(); + onColorChanged(); - GuiComponent::setOpacity(opacity); + GuiComponent::setOpacity(opacity); } unsigned char TextComponent::getOpacity() const { - return mColor & 0x000000FF; + return (mColor & 0xFF); } void TextComponent::setText(const std::string& text) { - mText = text; - onTextChanged(); + mText = text; + onTextChanged(); } void TextComponent::setUppercase(bool uppercase) { - mUppercase = uppercase; - onTextChanged(); + mUppercase = uppercase; + onTextChanged(); } -void TextComponent::render(const Transform4x4f& parentTrans) +void TextComponent::setHorizontalAlignment(Alignment align) { - if (!isVisible()) - return; - - Transform4x4f trans = parentTrans * getTransform(); - - if (mRenderBackground) - { - Renderer::setMatrix(trans); - Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), mBgColor, mBgColor); - } - - if(mTextCache) - { - const Vector2f& textSize = mTextCache->metrics.size; - float yOff = 0; - switch(mVerticalAlignment) - { - case ALIGN_TOP: - yOff = 0; - break; - case ALIGN_BOTTOM: - yOff = (getSize().y() - textSize.y()); - break; - case ALIGN_CENTER: - yOff = (getSize().y() - textSize.y()) / 2.0f; - break; - default: - LOG(LogError) << "Unknown vertical alignment: " << mVerticalAlignment; - break; - } - Vector3f off(0, yOff, 0); - - if(Settings::getInstance()->getBool("DebugText")) - { - // draw the "textbox" area, what we are aligned within - Renderer::setMatrix(trans); - Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0xFF000033, 0xFF000033); - } - - trans.translate(off); - Renderer::setMatrix(trans); - - // draw the text area, where the text actually is going - if(Settings::getInstance()->getBool("DebugText")) - { - switch(mHorizontalAlignment) - { - case ALIGN_LEFT: - Renderer::drawRect(0.0f, 0.0f, mTextCache->metrics.size.x(), mTextCache->metrics.size.y(), 0x00000033, 0x00000033); - break; - case ALIGN_CENTER: - Renderer::drawRect((mSize.x() - mTextCache->metrics.size.x()) / 2.0f, 0.0f, mTextCache->metrics.size.x(), mTextCache->metrics.size.y(), 0x00000033, 0x00000033); - break; - case ALIGN_RIGHT: - Renderer::drawRect(mSize.x() - mTextCache->metrics.size.x(), 0.0f, mTextCache->metrics.size.x(), mTextCache->metrics.size.y(), 0x00000033, 0x00000033); - break; - default: - LOG(LogError) << "Unknown horizontal alignment: " << mHorizontalAlignment; - break; - } - } - mFont->renderTextCache(mTextCache.get()); - } + mHorizontalAlignment = align; + onTextChanged(); } -std::string TextComponent::calculateExtent(bool allow_wrapping) +void TextComponent::setVerticalAlignment(Alignment align) { - std::string text = mUppercase ? Utils::String::toUpper(mText) : mText; - if(mAutoCalcExtent.x()) - { - mSize = mFont->sizeText(text, mLineSpacing); - }else if(mAutoCalcExtent.y() || allow_wrapping) - // usually a textcomponent wraps only when x > 0 and y == 0 in size (see TextComponent.h). - // The extra flag allow_wrapping does wrapping if an textcomponent has x > 0 and y > height of - // one line (calculated by fontsize and line spacing). - // Some themes rely on this wrap functionality while having an fixed y (y>0) in . - { - text = mFont->wrapText(text, getSize().x()); - if (mAutoCalcExtent.y()) { - // only resize when y was 0 before - // otherwise leave y value as defined before (i.e. theme value) - mSize.y() = mFont->sizeText(text, mLineSpacing).y(); - } - } - return text; + mVerticalAlignment = align; } -void TextComponent::onTextChanged() +void TextComponent::setLineSpacing(float spacing) { - if(!mFont || mText.empty()) - { - mTextCache.reset(); - return; - } - - std::shared_ptr f = mFont; - std::string text = calculateExtent(mSize.y() > f->getHeight(mLineSpacing)); - const bool oneLiner = mSize.y() > 0 && mSize.y() <= f->getHeight(mLineSpacing); - - if(oneLiner) - { - bool addAbbrev = false; - size_t newline = text.find('\n'); - text = text.substr(0, newline); // single line of text - stop at the first newline since it'll mess everything up - Vector2f size = f->sizeText(text); - addAbbrev = newline != std::string::npos || size.x() > mSize.x(); - - if(addAbbrev) - { - // abbreviate text - const std::string abbrev = "..."; - Vector2f abbrevSize = f->sizeText(abbrev); - - while(text.size() && size.x() + abbrevSize.x() > mSize.x()) - { - size_t newSize = Utils::String::prevCursor(text, text.size()); - text.erase(newSize, text.size() - newSize); - size = f->sizeText(text); - } - text.append(abbrev); - } - } - mTextCache = std::shared_ptr(f->buildTextCache(text, Vector2f(0, 0), (mColor >> 8 << 8) | mOpacity, mSize.x(), mHorizontalAlignment, mLineSpacing)); + mLineSpacing = spacing; + onTextChanged(); } -void TextComponent::onColorChanged() +void TextComponent::setValue(const std::string& value) { - if(mTextCache) - { - mTextCache->setColor(mColor); - } + setText(value); } -void TextComponent::setHorizontalAlignment(Alignment align) +std::string TextComponent::getValue() const { - mHorizontalAlignment = align; - onTextChanged(); + return mText; } -void TextComponent::setVerticalAlignment(Alignment align) +void TextComponent::render(const Transform4x4f& parentTrans) { - mVerticalAlignment = align; + if (!isVisible() || !mFont) + return; + + Transform4x4f trans = parentTrans * getTransform(); + Renderer::setMatrix(trans); + + // =============================== + // FONDO DEL TEXTO SI CORRESPONDE + // =============================== + if (mRenderBackground && (mBgColorOpacity > 0)) + { + Renderer::drawRect(0, 0, mSize.x(), mSize.y(), mBgColor, mBgColor); + } + + if (!mTextCache) + return; + + const Vector2f& size = mTextCache->metrics.size; + + float yOff = 0.0f; + switch (mVerticalAlignment) + { + case ALIGN_TOP: yOff = 0; break; + case ALIGN_BOTTOM: yOff = (getSize().y() - size.y()); break; + case ALIGN_CENTER: yOff = (getSize().y() - size.y()) / 2.0f; break; + } + + Transform4x4f baseTrans = trans; + baseTrans.translate(Vector3f(0.0f, yOff, 0.0f)); + + // =============================== + // STROKE / BORDE + // =============================== + if (mStrokeSize > 0.0f && (mStrokeColorOpacity > 0)) + { + unsigned int originalColor = mColor; + + mTextCache->setColor(mStrokeColor); + + const float s = mStrokeSize; + + const Vector2f offsets[4] = { + Vector2f(-s, 0), + Vector2f( s, 0), + Vector2f(0, -s), + Vector2f(0, s) + }; + + for (int i = 0; i < 4; i++) + { + Transform4x4f strokeTrans = baseTrans; + strokeTrans.translate(Vector3f(offsets[i].x(), offsets[i].y(), 0)); + Renderer::setMatrix(strokeTrans); + mFont->renderTextCache(mTextCache.get()); + } + + mTextCache->setColor(originalColor); + } + + // =============================== + // TEXTO PRINCIPAL + // =============================== + Renderer::setMatrix(baseTrans); + mFont->renderTextCache(mTextCache.get()); } -void TextComponent::setLineSpacing(float spacing) +std::string TextComponent::calculateExtent(bool allow_wrapping) { - mLineSpacing = spacing; - onTextChanged(); + std::string text = mUppercase ? Utils::String::toUpper(mText) : mText; + + if (mAutoCalcExtent.x()) + { + mSize = mFont->sizeText(text, mLineSpacing); + } + else if (mAutoCalcExtent.y() || allow_wrapping) + { + text = mFont->wrapText(text, getSize().x()); + if (mAutoCalcExtent.y()) + mSize.y() = mFont->sizeText(text, mLineSpacing).y(); + } + + return text; } -void TextComponent::setValue(const std::string& value) +void TextComponent::onTextChanged() { - setText(value); + if (!mFont || mText.empty()) + { + mTextCache.reset(); + return; + } + + std::string text = calculateExtent(mSize.y() > mFont->getHeight(mLineSpacing)); + const bool oneLine = (mSize.y() > 0 && mSize.y() <= mFont->getHeight(mLineSpacing)); + + if (oneLine) + { + bool shorten = false; + size_t newline = text.find('\n'); + text = text.substr(0, newline); + Vector2f ts = mFont->sizeText(text); + shorten = newline != std::string::npos || ts.x() > mSize.x(); + + if (shorten) + { + const std::string dots = "..."; + Vector2f dotsSize = mFont->sizeText(dots); + + while (text.size() && (ts.x() + dotsSize.x() > mSize.x())) + { + size_t newSize = Utils::String::prevCursor(text, text.size()); + text.erase(newSize); + ts = mFont->sizeText(text); + } + text.append(dots); + } + } + + mTextCache = std::shared_ptr( + mFont->buildTextCache( + text, + Vector2f(0, 0), + (mColor >> 8 << 8) | mOpacity, + mSize.x(), + mHorizontalAlignment, + mLineSpacing + )); } -std::string TextComponent::getValue() const +void TextComponent::onColorChanged() { - return mText; + if (mTextCache) + mTextCache->setColor(mColor); } -void TextComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) +void TextComponent::applyTheme(const std::shared_ptr& 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, "text"); - if(!elem) - return; - - if (properties & COLOR && elem->has("color")) - setColor(elem->get("color")); - - setRenderBackground(false); - if (properties & COLOR && elem->has("backgroundColor")) { - setBackgroundColor(elem->get("backgroundColor")); - setRenderBackground(true); - } - - if(properties & ALIGNMENT && elem->has("alignment")) - { - std::string str = elem->get("alignment"); - if(str == "left") - setHorizontalAlignment(ALIGN_LEFT); - else if(str == "center") - setHorizontalAlignment(ALIGN_CENTER); - else if(str == "right") - setHorizontalAlignment(ALIGN_RIGHT); - else - LOG(LogError) << "Unknown text alignment string: " << str; - } - - if(properties & TEXT && elem->has("text")) - setText(elem->get("text")); - - if(properties & FORCE_UPPERCASE && elem->has("forceUppercase")) - setUppercase(elem->get("forceUppercase")); - - if(properties & LINE_SPACING && elem->has("lineSpacing")) - setLineSpacing(elem->get("lineSpacing")); - - setFont(Font::getFromTheme(elem, properties, mFont)); + GuiComponent::applyTheme(theme, view, element, properties); + + using namespace ThemeFlags; + const ThemeData::ThemeElement* elem = theme->getElement(view, element, "text"); + if (!elem) + return; + + if (properties & COLOR && elem->has("color")) + setColor(elem->get("color")); + + if (elem->has("backgroundColor")) + setBackgroundColor(elem->get("backgroundColor")); + + if (properties & ALIGNMENT && elem->has("alignment")) + { + std::string a = elem->get("alignment"); + if (a == "left") setHorizontalAlignment(ALIGN_LEFT); + else if (a == "center") setHorizontalAlignment(ALIGN_CENTER); + else if (a == "right") setHorizontalAlignment(ALIGN_RIGHT); + } + + if (properties & TEXT && elem->has("text")) + setText(elem->get("text")); + + if (properties & FORCE_UPPERCASE && elem->has("forceUppercase")) + setUppercase(elem->get("forceUppercase")); + + if (properties & LINE_SPACING && elem->has("lineSpacing")) + setLineSpacing(elem->get("lineSpacing")); + + // STROKE (borde de texto) + if (elem->has("textStrokeColor")) + setTextStrokeColor(elem->get("textStrokeColor")); + + if (elem->has("textStrokeSize")) + setTextStrokeSize(elem->get("textStrokeSize")); + + setFont(Font::getFromTheme(elem, properties, mFont)); } diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index c464ef1140..919f8a222f 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -30,6 +30,10 @@ class TextComponent : public GuiComponent void setBackgroundColor(unsigned int color); void setRenderBackground(bool render); + // Stroke / outline del texto + void setTextStrokeColor(unsigned int color); + void setTextStrokeSize(float size); + void render(const Transform4x4f& parentTrans) override; std::string getValue() const override; @@ -55,8 +59,10 @@ class TextComponent : public GuiComponent unsigned int mColor; unsigned int mBgColor; + unsigned int mStrokeColor; unsigned char mColorOpacity; unsigned char mBgColorOpacity; + unsigned char mStrokeColorOpacity; bool mRenderBackground; bool mUppercase; @@ -65,6 +71,8 @@ class TextComponent : public GuiComponent Alignment mHorizontalAlignment; Alignment mVerticalAlignment; float mLineSpacing; + + float mStrokeSize; }; #endif // ES_CORE_COMPONENTS_TEXT_COMPONENT_H diff --git a/es-core/src/guis/GuiMsgBox.cpp b/es-core/src/guis/GuiMsgBox.cpp index 9db9ffff11..8007e76ea9 100644 --- a/es-core/src/guis/GuiMsgBox.cpp +++ b/es-core/src/guis/GuiMsgBox.cpp @@ -1,110 +1,145 @@ +// es-core/src/guis/GuiMsgBox.cpp + #include "guis/GuiMsgBox.h" #include "components/ButtonComponent.h" #include "components/MenuComponent.h" +#include "../LocaleESHook.h" // Para es_translate #define HORIZONTAL_PADDING_PX 20 GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, - const std::string& name1, const std::function& func1, - const std::string& name2, const std::function& func2, - const std::string& name3, const std::function& func3) : GuiComponent(window), - mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 2)) + const std::string& name1, const std::function& func1, + const std::string& name2, const std::function& func2, + const std::string& name3, const std::function& func3) + : GuiComponent(window) + , mBackground(window, ":/frame.png") + , mGrid(window, Vector2i(1, 2)) { - float width = Renderer::getScreenWidth() * 0.6f; // max width - float minWidth = Renderer::getScreenWidth() * 0.3f; // minimum width - - mMsg = std::make_shared(mWindow, text, Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); - mGrid.setEntry(mMsg, Vector2i(0, 0), false, false); - - // create the buttons - mButtons.push_back(std::make_shared(mWindow, name1, name1, std::bind(&GuiMsgBox::deleteMeAndCall, this, func1))); - if(!name2.empty()) - mButtons.push_back(std::make_shared(mWindow, name2, name3, std::bind(&GuiMsgBox::deleteMeAndCall, this, func2))); - if(!name3.empty()) - mButtons.push_back(std::make_shared(mWindow, name3, name3, std::bind(&GuiMsgBox::deleteMeAndCall, this, func3))); - - // set accelerator automatically (button to press when "b" is pressed) - if(mButtons.size() == 1) - { - mAcceleratorFunc = mButtons.front()->getPressedFunc(); - }else{ - for(auto it = mButtons.cbegin(); it != mButtons.cend(); it++) - { - if(Utils::String::toUpper((*it)->getText()) == "OK" || Utils::String::toUpper((*it)->getText()) == "NO") - { - mAcceleratorFunc = (*it)->getPressedFunc(); - break; - } - } - } - - // put the buttons into a ComponentGrid - mButtonGrid = makeButtonGrid(mWindow, mButtons); - mGrid.setEntry(mButtonGrid, Vector2i(0, 1), true, false, Vector2i(1, 1), GridFlags::BORDER_TOP); - - // decide final width - if(mMsg->getSize().x() < width && mButtonGrid->getSize().x() < width) - { - // mMsg and buttons are narrower than width - width = Math::max(mButtonGrid->getSize().x(), mMsg->getSize().x()); - width = Math::max(width, minWidth); - } - - // now that we know width, we can find height - mMsg->setSize(width, 0); // mMsg->getSize.y() now returns the proper length - const float msgHeight = Math::max(Font::get(FONT_SIZE_LARGE)->getHeight(), mMsg->getSize().y()*1.225f); - setSize(width + HORIZONTAL_PADDING_PX*2, msgHeight + mButtonGrid->getSize().y()); - - // center for good measure - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2.0f, (Renderer::getScreenHeight() - mSize.y()) / 2.0f); - - addChild(&mBackground); - addChild(&mGrid); + float width = Renderer::getScreenWidth() * 0.6f; // max width + float minWidth = Renderer::getScreenWidth() * 0.3f; // minimum width + + // Texto del mensaje traducido + mMsg = std::make_shared( + mWindow, + es_translate(text), + Font::get(FONT_SIZE_MEDIUM), + 0x777777FF, + ALIGN_CENTER + ); + mGrid.setEntry(mMsg, Vector2i(0, 0), false, false); + + // Botones traducidos + if (!name1.empty()) + mButtons.push_back(std::make_shared( + mWindow, + es_translate(name1), + name1, + std::bind(&GuiMsgBox::deleteMeAndCall, this, func1))); + + if (!name2.empty()) + mButtons.push_back(std::make_shared( + mWindow, + es_translate(name2), + name2, + std::bind(&GuiMsgBox::deleteMeAndCall, this, func2))); + + if (!name3.empty()) + mButtons.push_back(std::make_shared( + mWindow, + es_translate(name3), + name3, + std::bind(&GuiMsgBox::deleteMeAndCall, this, func3))); + + // Botón “acelerador” (B / Enter / etc.) + if (mButtons.size() == 1) + { + mAcceleratorFunc = mButtons.front()->getPressedFunc(); + } + else + { + for (auto& btn : mButtons) + { + std::string txtUpper = Utils::String::toUpper(btn->getText()); + if (txtUpper == es_translate("OK") || txtUpper == es_translate("NO") || + txtUpper == "OK" || txtUpper == "NO") + { + mAcceleratorFunc = btn->getPressedFunc(); + break; + } + } + } + + // Grid de botones + mButtonGrid = makeButtonGrid(mWindow, mButtons); + mGrid.setEntry(mButtonGrid, Vector2i(0, 1), true, false, + Vector2i(1, 1), GridFlags::BORDER_TOP); + + // Ajuste de ancho + if (mMsg->getSize().x() < width && mButtonGrid->getSize().x() < width) + { + width = Math::max(mButtonGrid->getSize().x(), mMsg->getSize().x()); + width = Math::max(width, minWidth); + } + + mMsg->setSize(width, 0); + const float msgHeight = Math::max( + Font::get(FONT_SIZE_LARGE)->getHeight(), + mMsg->getSize().y() * 1.225f + ); + + setSize(width + HORIZONTAL_PADDING_PX * 2, + msgHeight + mButtonGrid->getSize().y()); + + setPosition( + (Renderer::getScreenWidth() - mSize.x()) / 2.0f, + (Renderer::getScreenHeight() - mSize.y()) / 2.0f + ); + + addChild(&mBackground); + addChild(&mGrid); } bool GuiMsgBox::input(InputConfig* config, Input input) { - // special case for when GuiMsgBox comes up to report errors before anything has been configured - if(config->getDeviceId() == DEVICE_KEYBOARD && !config->isConfigured() && input.value && - (input.id == SDLK_RETURN || input.id == SDLK_ESCAPE || input.id == SDLK_SPACE)) - { - mAcceleratorFunc(); - return true; - } - - if(mAcceleratorFunc && config->isMappedTo("b", input) && input.value != 0) - { - mAcceleratorFunc(); - return true; - } - - return GuiComponent::input(config, input); + if (config->getDeviceId() == DEVICE_KEYBOARD && !config->isConfigured() && + input.value && + (input.id == SDLK_RETURN || input.id == SDLK_ESCAPE || input.id == SDLK_SPACE)) + { + if (mAcceleratorFunc) + mAcceleratorFunc(); + return true; + } + + if (mAcceleratorFunc && config->isMappedTo("b", input) && input.value != 0) + { + mAcceleratorFunc(); + return true; + } + + return GuiComponent::input(config, input); } void GuiMsgBox::onSizeChanged() { - mGrid.setSize(mSize); - mGrid.setRowHeightPerc(1, mButtonGrid->getSize().y() / mSize.y()); + mGrid.setSize(mSize); + mGrid.setRowHeightPerc(1, mButtonGrid->getSize().y() / mSize.y()); - // update messagebox size - mMsg->setSize(mSize.x() - HORIZONTAL_PADDING_PX*2, mGrid.getRowHeight(0)); - mGrid.onSizeChanged(); + mMsg->setSize(mSize.x() - HORIZONTAL_PADDING_PX * 2, mGrid.getRowHeight(0)); + mGrid.onSizeChanged(); - mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); + mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); } void GuiMsgBox::deleteMeAndCall(const std::function& func) { - auto funcCopy = func; - delete this; - - if(funcCopy) - funcCopy(); - + auto funcCopy = func; + delete this; + if (funcCopy) + funcCopy(); } std::vector GuiMsgBox::getHelpPrompts() { - return mGrid.getHelpPrompts(); + return mGrid.getHelpPrompts(); } diff --git a/es-core/src/resources/ThemeSounds.cpp b/es-core/src/resources/ThemeSounds.cpp new file mode 100644 index 0000000000..334f4f6fa6 --- /dev/null +++ b/es-core/src/resources/ThemeSounds.cpp @@ -0,0 +1,14 @@ +#include "ThemeSounds.h" +#include "ThemeData.h" + +// De momento no hacemos nada aquí porque la implementación está en el header +// Solo dejamos las definiciones vacías para que el linker esté contento +void ThemeSounds::loadFrom(const ThemeData& /*theme*/) +{ + // En el futuro: leer rutas de sonidos desde ThemeData y guardarlas en estáticos +} + +void ThemeSounds::play(ThemeSoundId /*id*/) +{ + // En el futuro: usar AudioManager/Sound para reproducir los sonidos cargados +} diff --git a/es-core/src/resources/ThemeSounds.h b/es-core/src/resources/ThemeSounds.h new file mode 100644 index 0000000000..9b43c17de6 --- /dev/null +++ b/es-core/src/resources/ThemeSounds.h @@ -0,0 +1,30 @@ +#pragma once + +// Declaración adelantada para evitar dependencias circulares +class ThemeData; + +// Identificadores de sonidos del tema +enum ThemeSoundId +{ + THEME_SOUND_SCROLL = 0, + THEME_SOUND_SELECT = 1, + THEME_SOUND_BACK = 2 +}; + +// Implementación mínima para que TODO compile sin romper nada. +// Más adelante se puede completar con carga real de sonidos. +class ThemeSounds +{ +public: + // Cargar sonidos desde el theme (por ahora no hace nada) + static void loadFrom(const ThemeData& /*theme*/) + { + // TODO: Implementar lógica real de lectura de sonidos desde ThemeData + } + + // Reproducir un sonido del theme (por ahora no hace nada) + static void play(ThemeSoundId /*id*/) + { + // TODO: Implementar lógica real de reproducción más adelante + } +}; diff --git a/es-core/src/resources/fonts/players.ttf b/es-core/src/resources/fonts/players.ttf new file mode 100755 index 0000000000..9e83c589d9 Binary files /dev/null and b/es-core/src/resources/fonts/players.ttf differ diff --git a/lang/es.ini b/lang/es.ini new file mode 100644 index 0000000000..66a6fcf2b3 --- /dev/null +++ b/lang/es.ini @@ -0,0 +1,315 @@ +[META] +CODE=es +NAME=Español + + +[MAIN] +MAIN MENU=MENÚ PRINCIPAL +QUIT=SALIR +YES=SÍ +NO=NO +OK=ACEPTAR +BACK=VOLVER +START=INICIO +SELECT=SELECCIONAR +CANCEL=CANCELAR + + +[GAME LIST] +OPTIONS=OPCIONES +JUMP TO ...=IR A ... +LAUNCH SYSTEM SCREENSAVER=INICIAR SCREENSAVER DEL SISTEMA +SORT GAMES BY=ORDENAR JUEGOS POR +FILTER GAMELIST=FILTRAR LISTA DE JUEGOS +ADD/REMOVE GAMES TO THIS GAME COLLECTION=AGREGAR/QUITAR JUEGOS A ESTA COLECCIÓN +FINISH EDITING=FINALIZAR EDICIÓN +COLLECTION=COLECCIÓN +COLLECTIONS=COLECCIONES +ALL GAMES=TODOS LOS JUEGOS +GET NEW RANDOM GAMES=OBTENER NUEVOS JUEGOS ALEATORIOS +EDIT THIS FOLDER'S METADATA=EDITAR METADATOS DE CARPETA +EDIT THIS GAME'S METADATA=EDITAR METADATOS DE JUEGO +CLOSE=CERRAR + + +[SCRAPER] +SCRAPER=SCRAPER +SCRAPE FROM=FUENTE DE DATOS +SCRAPE RATINGS=OBTENER PUNTUACIONES +SCRAPE NOW=BUSCAR AHORA + + +[SETTINGS] +SOUND SETTINGS=SONIDO +UI SETTINGS=INTERFAZ +OTHER SETTINGS=OTRAS OPCIONES +GAME COLLECTION SETTINGS=COLECCIONES +SCREEN RESOLUTION=RESOLUCIÓN DE PANTALLA +ENABLE NAVIGATION SOUNDS=SONIDOS DE MENÚ +ENABLE VIDEO AUDIO=AUDIO DE VIDEO +SYSTEM VOLUME=VOLUMEN DEL SISTEMA +THEME SET=TEMA +TRANSITION STYLE=ESTILO DE TRANSICIÓN +CAROUSEL TRANSITIONS=TRANSICIONES DE CARRUSEL +QUICK SYSTEM SELECT=CAMBIO RÁPIDO DE SISTEMA +UI MODE=MODO DE INTERFAZ +SCREENSAVER SETTINGS=AJUSTES DE SCREENSAVER +ON-SCREEN HELP=AYUDA EN PANTALLA +ENABLE FILTERS=HABILITAR FILTROS +USE FULL SCREEN PAGING FOR LB/RB=PAGINADO COMPLETO PARA LB/RB +START ON SYSTEM=INICIAR EN SISTEMA +SHOW FRAMERATE=MOSTRAR FPS +AUDIO CARD=TARJETA DE AUDIO +AUDIO DEVICE=DISPOSITIVO DE AUDIO +POWER SAVER MODES=MODOS DE AHORRO DE ENERGÍA +VRAM LIMIT=LÍMITE DE VRAM +PARSE GAMESLISTS ONLY=SOLO LEER LISTAS DE JUEGOS +SEARCH FOR LOCAL ART=BUSCAR ARTE LOCAL +SHOW HIDDEN FILES=MOSTRAR ARCHIVOS OCULTOS +INDEX FILES DURING SCREENSAVER=INDEXAR ARCHIVOS EN SCREENSAVER +BACKGROUND INDEXING=INDEXADO EN SEGUNDO PLANO +SAVE METADATA=GUARDAR METADATOS +IGNORE ARTICLES (NAME SORT ONLY)=IGNORAR ARTÍCULOS (SOLO ORDEN POR NOMBRE) +DISABLE START MENU IN KID MODE=OCULTAR MENÚ START EN MODO NIÑOS +USE OMX PLAYER (HW ACCELERATED)=USAR OMX PLAYER (ACELERADO HW) +DARK MENU=MENÚ OSCURO +CLOCK=RELOJ +THEME OPTIONS=OPCIONES DEL TEMA + + +[LANGUAGE] +LANGUAGE=IDIOMA + + +[META] +EDIT METADATA=EDITAR METADATOS +GAME=JUEGO +FOLDER=CARPETA +NAME=NOMBRE +ENTER GAME NAME=NOMBRE DEL JUEGO +DESCRIPTION=DESCRIPCIÓN +ENTER DESCRIPTION=INGRESE UNA DESCRIPCIÓN +RELEASE DATE=FECHA DE LANZAMIENTO +ENTER RELEASE DATE=INGRESE FECHA DE LANZAMIENTO +PLAY COUNT=VECES JUGADO +ENTER NUMBER OF TIMES PLAYED=INGRESE CANTIDAD JUGADA +LAST PLAYED=ÚLTIMA VEZ JUGADO +ENTER LAST PLAYED DATE=INGRESE FECHA DE ÚLTIMA VEZ + +SCRAPE=OBTENER DATOS +SAVE=GUARDAR +CANCEL=CANCELAR +DELETE=ELIMINAR +THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?=¡ESTO ELIMINARÁ EL/LOS ARCHIVO(S) DE JUEGO!\n¿ESTÁS SEGURO? +SAVE CHANGES?=¿GUARDAR CAMBIOS? +YES=SÍ +NO=NO +BACK=VOLVER +CLOSE=CERRAR + + +[VIEW] +SYSTEM INFO=INFO DEL SISTEMA +CONFIGURATION=CONFIGURACIÓN +GAME=JUEGO +GAMES=JUEGOS +AVAILABLE=DISPONIBLES +CHOOSE=ELEGIR +SELECT=SELECCIONAR +RANDOM=ALEATORIO +LAUNCH SCREENSAVER=INICIAR SCREENSAVER + + +[GAMELIST] +GAMELIST VIEW STYLE=ESTILO DE VISTA +automatic=automático +basic=básico +detailed=detallado +video=video +grid=cuadrícula + + +[GAME DATA] +RATING=RATING +RELEASE DATE=FECHA +DEVELOPER=DEV. +PUBLISHER=DIST. +GENRE=GÉN. +PLAY COUNT=VECES +LAST PLAYED=ÚLT. VEZ +PLAYERS=JUG. +FAVORITES=FAVS. + + +[MESSAGES] +REALLY RESTART?=¿REINICIAR? +REALLY QUIT?=¿SALIR? +REALLY SHUTDOWN?=¿APAGAR? +FILES SORTED=ARCHIVOS ORDENADOS +You are changing the UI to a restricted mode:=Estás cambiando la interfaz a un modo restringido: +This will hide most menu-options to prevent changes to the system.=Esto ocultará la mayoría de las opciones para evitar cambios en el sistema. +To unlock and return to the full UI, enter this code:=Para desbloquear y volver a la interfaz completa, introduce este código: +Do you want to proceed?=¿Deseas continuar? + + +[GAME COLLECTION] +GAME COLLECTION SETTINGS=AJUSTES DE COLECCIONES +AUTOMATIC GAME COLLECTIONS=COLECCIONES AUTOMÁTICAS +CUSTOM GAME COLLECTIONS=COLECCIONES PERSONALIZADAS +RANDOM GAME COLLECTION SETTINGS=AJUSTES DE COLECCIÓN ALEATORIA +CREATE NEW CUSTOM COLLECTION FROM THEME=CREAR COLECCIÓN DESDE TEMA +CREATE NEW CUSTOM COLLECTION=CREAR NUEVA COLECCIÓN +GROUP UNTHEMED CUSTOM COLLECTIONS=AGRUPAR COLECCIONES SIN TEMA +SORT CUSTOM COLLECTIONS AND SYSTEMS=ORDENAR COLECCIONES Y SISTEMAS +1 SELECTED=1 SELECCIONADA +9 SELECTED=9 SELECCIONADAS +FINISH EDITING=TERMINAR EDICIÓN +COLLECTION=COLECCIÓN +SELECT THEME FOLDER=SELECCIONAR CARPETA DE TEMA +New Collection Name=Nombre de la colección +SHOW SYSTEM NAME IN COLLECTIONS=MOSTRAR SISTEMA +PRESS (Y) TWICE TO REMOVE FROM FAVS./COLL.=PRESIONA (Y) DOS VECES PARA QUITAR DE FAVS./COLL. +ADD/REMOVE GAMES WHILE SCREENSAVER TO=AÑADIR/QUITAR JUEGOS EN SCREENSAVER A += +SELECT COLLECTIONS=SELECCIONAR COLECCIONES + + +[COLLECTIONS] +all games=TODOS LOS JUEGOS +last played=ÚLTIMOS JUGADOS +favorites=FAVORITOS +collections=COLECCIONES +random=ALEATORIOS + + +[GAMELIST_DATA] +RELEASED=LANZAMIENTO +LAST PLAYED=ÚLT. VEZ +TIMES PLAYED=VECES +PLAYERS=JUGADORES +OPTIONS=OPCIONES +MENU=MENÚ +BACK=VOLVER +LAUNCH=INICIAR +SYSTEM=SISTEMA +CHOOSE=ELEGIR +FAVORITES=FAVORITOS +RANDOM=ALEATORIO + + +[HELP] +CHOOSE=ELEGIR +MENU=MENÚ +RANDOM=ALEATORIO +SELECT=SELECCIONAR +BACK=VOLVER +OPTIONS=OPCIONES +FAVORITE=FAVORITO +FAVORITES=FAVORITOS +TOGGLE FAVORITE=MARCAR/DESMARCAR FAVORITO +ADD/REMOVE FAVORITE=AÑADIR/QUITAR FAVORITO +LAUNCH SCREENSAVER=ACTIVAR SCREENSAVER +CHANGE=CAMBIAR + + +[GENERAL] +DEFAULT=PREDETERMINADO +CUSTOM=PERSONALIZADO +SYSTEM=SISTEMA +OPTIONS=OPCIONES +FAVORITES=FAVORITOS +NONE=NINGUNO + + +[RELATIVE TIME] +DAYS_AGO=%d días atrás +HOURS_AGO=%d horas atrás +MINS_AGO=%d min atrás +SECS_AGO=%d seg atrás +NEVER=NUNCA +UNKNOWN=DESCONOCIDO + + +[SCRAPER_MULTI] +SCRAPING IN PROGRESS=OBTENIENDO DATOS... +INPUT=ENTRAR TEXTO +SEARCH=BUSCAR +SKIP=OMITIR +STOP=DETENER +STOP (PROGRESS SAVED)=DETENER (GUARDANDO PROGRESO) +GAME SCRAPED=JUEGO ACTUALIZADO +GAMES SCRAPED=JUEGOS ACTUALIZADOS +GAME SKIPPED=JUEGO OMITIDO +GAMES SKIPPED=JUEGOS OMITIDOS +NO GAMES WERE SCRAPED.=NINGÚN JUEGO FUE ACTUALIZADO. +OF=DE + + +[COLLECTION_POPUPS] +EDITING_COLLECTION=Editando la colección '{collection}'. Añade o quita juegos con (Y). +FINISHED_EDITING_COLLECTION=Edición de la colección '{collection}' finalizada. +PRESS_AGAIN_TO_REMOVE=Pulsa de nuevo para quitar de '{collection}'. +ADDED_TO_COLLECTION=Se agregó '{game}' a '{collection}'. +REMOVED_FROM_COLLECTION=Se eliminó '{game}' de '{collection}'. +EMPTY_COLLECTION=Esta colección está vacía. + + +[COLLECTION_DIALOG] +SELECT ALL=SELECCIONAR TODO +SELECT NONE=NO SELECCIONAR NINGUNA + + +[DATE] +NEVER=NUNCA +UNKNOWN=DESCONOCIDO +DAYS AGO=DÍAS ATRÁS +HOURS AGO=HORAS ATRÁS +MINUTES AGO=MINUTOS ATRÁS +SECONDS AGO=SEGUNDOS ATRÁS + + +[BUTTONS] +A=ACEPTAR +B=VOLVER +X=ALEATORIO +Y=FAVORITOS +START=INICIO +SELECT=SELECCIONAR +CHANGE=CAMBIAR + + +[SYSTEMVIEW] +ARCADE=ARCADE +RETROPIE=RETROPIE +GAMES AVAILABLE=JUEGOS DISPONIBLES + + +[QUIT] +QUIT EMULATIONSTATION=SALIR DE EMULATIONSTATION +RESTART EMULATIONSTATION=REINICIAR EMULATIONSTATION +RESTART SYSTEM=REINICIAR SISTEMA +SHUTDOWN SYSTEM=APAGAR SISTEMA + + +[ERROR] +ERROR=ERROR +NETWORK ERROR=ERROR DE RED +NOT FOUND=NO ENCONTRADO + + +[INPUT] +CONFIGURE INPUT=CONFIGURAR ENTRADAS +ARE YOU SURE YOU WANT TO CONFIGURE INPUT?=¿CONFIGURAR ENTRADAS? + + +[SYSTEM] +SYSTEM SETTINGS=AJUSTES DEL SISTEMA + + +[LOWERCASE] +choose=ELEGIR +select=SELECCIONAR +close=CERRAR +back=volver +dark menu=menú oscuro +change=cambiar diff --git a/lang/fr.ini b/lang/fr.ini new file mode 100644 index 0000000000..0834479759 --- /dev/null +++ b/lang/fr.ini @@ -0,0 +1,314 @@ +[META] +CODE=fr +NAME=Français + + +[MAIN] +MAIN MENU=MENU PRINCIPAL +QUIT=QUITTER +YES=OUI +NO=NON +OK=OK +BACK=RETOUR +START=START +SELECT=SELECT +CANCEL=ANNULER + + +[GAME LIST] +OPTIONS=OPTIONS +JUMP TO ...=ALLER A... +LAUNCH SYSTEM SCREENSAVER=LANCER L'ECRAN DE VEILLE +SORT GAMES BY=TRIER LES JEUX PAR +FILTER GAMELIST=FILTRER LA LISTE DE JEU +ADD/REMOVE GAMES TO THIS GAME COLLECTION=AJOUTER/ENLEVER DES JEUX DANS CETTE COLLECTION +FINISH EDITING=VALIDER L'EDITION +COLLECTION=COLLECTION +COLLECTIONS=COLLECTIONS +ALL GAMES=TOUS LES JEUX +GET NEW RANDOM GAMES=OBTENIR DE NOUVEAUX JEUX ALEATOIREMENT +EDIT THIS FOLDER'S METADATA=EDITER LES METADONNEES DE CE DOSSIER +EDIT THIS GAME'S METADATA=EDITER LES METADONNEES DE CE JEU +CLOSE=FERMER + + +[SCRAPER] +SCRAPER=SCRAPER +SCRAPE FROM=SCRAPER A PARTIR DE +SCRAPE RATINGS=RECUPERER LES NOTES +SCRAPE NOW=SCRAPER MAINTENANT + + +[SETTINGS] +SOUND SETTINGS=PARAMETRES DU SON +UI SETTINGS=INTERFACE +OTHER SETTINGS=AUTRES PARAMETRES +GAME COLLECTION SETTINGS=GERER LES COLLECTIONS +SCREEN RESOLUTION=RESOLUTION DE L'ECRAN +ENABLE NAVIGATION SOUNDS=SONS DU MENU +ENABLE VIDEO AUDIO=SON DES APERCUS VIDEO +SYSTEM VOLUME=VOLUME GENERAL +THEME SET=THEMES +TRANSITION STYLE=STYLE DE TRANSITION +CAROUSEL TRANSITIONS=TRANSITION DU CARROUSEL +QUICK SYSTEM SELECT=CHANGEMENT RAPIDE DE CONSOLE +UI MODE=MODE DE L'INTERFACE +SCREENSAVER SETTINGS=PARAMETRE DE L'ECRAN DE VEILLE +ON-SCREEN HELP=AIDE A L'ECRAN +ENABLE FILTERS=ACTIVER LES FILTRES GRAPHIQUES +USE FULL SCREEN PAGING FOR LB/RB=DEFILEMENT PLUS RAPIDE AVEC LB/RB +START ON SYSTEM=DEMARRER SUR LA CONSOLE +SHOW FRAMERATE=AFFICHER LES IMAGES PAR SECONDE +AUDIO CARD=CARTE AUDIO +AUDIO DEVICE=MATERIEL AUDIO +POWER SAVER MODES=MODE ECONOMIE D'ENERGIE +VRAM LIMIT=LIMITE DE VRAM +PARSE GAMESLISTS ONLY=PARSER UNIQUEMENT LES GAMELISTS +SEARCH FOR LOCAL ART=RECHERCHER DES IMAGES EN LOCAL +SHOW HIDDEN FILES=AFFICHER LES FICHIERS CACHES +INDEX FILES DURING SCREENSAVER=INDEXER PENDANT LA MISE EN VEILLE +BACKGROUND INDEXING=INDEXER EN ARRIERE PLAN +SAVE METADATA=SAUVEGARDER LES METADONNEES +IGNORE ARTICLES (NAME SORT ONLY)=IGNORER LES ARTICLES (TRI ALPHABETIQUE) +DISABLE START MENU IN KID MODE=DESACTIVER CE MENU EN MODE KID +USE OMX PLAYER (HW ACCELERATED)=UTILISER OMX PLAYER(ACCELERATION MATERIELLE) +DARK MENU=MENU SOMBRE + + +[LANGUAGE] +LANGUAGE=LANGUE + + +[META] +EDIT METADATA=EDITER LES METADONNEES +GAME=JEU +FOLDER=DOSSIER +NAME=NOM +ENTER GAME NAME=TAPER LE NOM DU JEU +DESCRIPTION=DESCRIPTION +ENTER DESCRIPTION=VALIDER LA DESCRIPTION +RELEASE DATE=DATE DE SORTIE +ENTER RELEASE DATE=VALIDER LA DATE DE SORTIE +PLAY COUNT=NOMBRE DE PARTIES JOUEES +ENTER NUMBER OF TIMES PLAYED=VALIDER LE NOMBRE DE PARTIES JOUEES +LAST PLAYED=JOUE LA DERNIERE FOIS +ENTER LAST PLAYED DATE=ENTRER LA DATE DU DERNIER LANCEMENT + +SCRAPE=SCRAPER +SAVE=SAUVEGARDER +CANCEL=ANNULER +DELETE=SUPPRIMER +THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?=TOUS LES FICHIERS DU JEU SERONT SUPPRIMES ! ES-TU CERTAIN ? +SAVE CHANGES?=SAUVEGARDER LES CHANGEMENTS ? +YES=OUI +NO=NON +BACK=RETOUR +CLOSE=FERMER + + +[VIEW] +SYSTEM INFO=INFO SYSTEME +CONFIGURATION=CONFIGURATION +GAME=JEU +GAMES=JEUX +AVAILABLE=DISPONIBLES +CHOOSE=CHOISIR +SELECT=SELECTIONNER +RANDOM=ALEATOIRE +LAUNCH SCREENSAVER=DEMARRER L'ECRAN DE VEILLE + + +[GAMELIST] +GAMELIST VIEW STYLE=STYLE DES LISTES DE JEUX +automatic=automatique +basic=basic +detailed=détaillé +video=aperçus vidéo +grid=grille + + +[GAME DATA] +RATING=NOTE +RELEASE DATE=DATE DE SORTIE +DEVELOPER=DEVELOPPEUR +PUBLISHER=EDITEUR +GENRE=GENRE +PLAY COUNT=NOMBRE DE FOIS JOUE +LAST PLAYED=JOUE DERNIEREMENT +PLAYERS=JOUEURS +FAVORITES=FAVORIS + + +[MESSAGES] +REALLY RESTART?=REDEMARRER ? +REALLY QUIT?=FERMER ? +REALLY SHUTDOWN?=ETEINDRE ? +FILES SORTED=FICHIERS TRIES +You are changing the UI to a restricted mode:=L'INTERFACE VA PASSER DANS UN MODE RESTREINT +This will hide most menu-options to prevent changes to the system.=LA PLUPART DES OPTIONS SERONT CACHEES POUR EVITER DE CASSER LE SYSTEME. +To unlock and return to the full UI, enter this code:=POUR DEBLOQUER LE MENU COMPLET, TAPE CE CODE : +Do you want to proceed?=VEUX-TU CONTINUER ? + + +[GAME COLLECTION] +GAME COLLECTION SETTINGS=GERER LES COLLECTIONS +AUTOMATIC GAME COLLECTIONS=COLLECTIONS SYSTEME +CUSTOM GAME COLLECTIONS=COLLECTIONS PERSONNALISEES +RANDOM GAME COLLECTION SETTINGS=AJUSTER LES COLLECTIONS ALEATOIRES +CREATE NEW CUSTOM COLLECTION FROM THEME=CREER UNE NOUVELLE COLLECTION A PARTIR DE... +CREATE NEW CUSTOM COLLECTION=CREER UNE NOUVELLE COLLECTION +GROUP UNTHEMED CUSTOM COLLECTIONS=GROUPER LES COLLECTIONS PERSO SANS THEME +SORT CUSTOM COLLECTIONS AND SYSTEMS=TRIER LES COLLECTIONS PERSO ET LES CONSOLES +1 SELECTED=1 SELECTIONNEE +9 SELECTED=9 SELECTIONNEES +FINISH EDITING=TERMINER L'EDITION DE +COLLECTION=COLLECTION +SELECT THEME FOLDER=SELECTIONNER LE THEME DU DOSSIER +New Collection Name=NOM DE LA NOUVELLE COLLECTION +SHOW SYSTEM NAME IN COLLECTIONS=AFFICHER LE NOM DES CONSOLES DANS LES LISTES +PRESS (Y) TWICE TO REMOVE FROM FAVS./COLL.=PRESSER DEUX FOIS (Y) POUR ENLEVER DES FAV/COLLEC +ADD/REMOVE GAMES WHILE SCREENSAVER TO=AJOUTER/ENLEVER DES JEUX PENDANT LA VEILLE += +SELECT COLLECTIONS=SELECTIONNER LES COLLECTIONS + + +[COLLECTIONS] +all games=TOUS LES JEUX +last played=JOUES RECEMMENT +favorites=FAVORIS +collections=COLLECTIONS +random=ALEATOIRE + + +[GAMELIST_DATA] +RELEASED=DATE DE SORTIE +LAST PLAYED=JOUE DERNIEREMENT +TIMES PLAYED=NOMBRE DE FOIS JOUE +PLAYERS=NOMBRE DE JOUEURS +OPTIONS=OPTIONS +MENU=MENU +BACK=RETOUR +LAUNCH=LANCER +SYSTEM=SYSTEME +CHOOSE=CHOISIR +FAVORITES=FAVORIS +RANDOM=ALEATOIRE + + +[HELP] +CHOOSE=CHOISIR +MENU=MENU +RANDOM=ALEATOIRE +SELECT=SELECTIONNER +BACK=RETOUR +OPTIONS=OPTIONS +FAVORITE=FAVORI +FAVORITES=FAVORIS +TOGGLE FAVORITE=COCHER/DECOCHER FAVORI +ADD/REMOVE FAVORITE=AJOUTER/SUPPRIMER FAVORI +LAUNCH SCREENSAVER=LANCER L'ECRAN DE VEILLE +CHANGE=CHANGER + + +[GENERAL] +DEFAULT=PAR DEFAUT +CUSTOM=PERSONNALISER +SYSTEM=SYSTEME +OPTIONS=OPTIONS +FAVORITES=FAVORIS +NONE=VIDE + + +[RELATIVE TIME] +DAYS_AGO=IL Y A %d JOURS +HOURS_AGO=IL Y A %d HEURES +MINS_AGO=IL A %d MINUTES +SECS_AGO=IL Y A %d SECONDES +NEVER=JAMAIS +UNKNOWN=INCONNU + + +[SCRAPER_MULTI] +SCRAPING IN PROGRESS=TELECHARGEMENT DES DONNEE... +INPUT=TAPER TEXTE +SEARCH=CHERCHER +SKIP=PASSER +STOP=ARRETER +STOP (PROGRESS SAVED)=ARRETER (PROGRESSION SAUVEGARDEE) +GAME SCRAPED=JEU SCRAPE +GAMES SCRAPED=JEUX SCRAPES +GAME SKIPPED=JEU IGNORE +GAMES SKIPPED=JEUX IGNORES +NO GAMES WERE SCRAPED.=AUCUN JEU N'A ETE SCRAPE +OF=DE + + +[COLLECTION_POPUPS] +EDITING_COLLECTION=EDITION DE LA COLLECTION +FINISHED_EDITING_COLLECTION=FIN DE L'EDITION DE +PRESS_AGAIN_TO_REMOVE=APPUYER UNE SECONDE FOIS POUR SUPPRIMER DE +ADDED_TO_COLLECTION=AJOUTE A LA COLLECTION +REMOVED_FROM_COLLECTION=ENLEVE DE LA COLLECTION +EMPTY_COLLECTION=COLLECTION VIDE + + +[COLLECTION_DIALOG] +SELECT ALL=SELECTIONNER TOUTES +SELECT NONE=SELECTIONNER AUCUNE + + +[DATE] +NEVER=JAMAIS +UNKNOWN=INCONNUS +DAYS AGO=JOURS +HOURS AGO=HEURES +MINUTES AGO=MINUTES +SECONDS AGO=SECONDES + + +[BUTTONS] +A=VALIDER +B=RETOUR +X=ALEATOIRE +Y=FAVORIS +START=START +SELECT=SELECT +CHANGE=CHANGER + + +[SYSTEMVIEW] +ARCADE=ARCADE +RETROPIE=RETROPIE +GAMES AVAILABLE=JEUX DISPONIBLES + + +[QUIT] +QUIT EMULATIONSTATION=FERMER EMULATIONSTATION +RESTART EMULATIONSTATION=REDEMARRER EMULATIONSTATION +RESTART SYSTEM=REDEMARRER LE SYSTEME +SHUTDOWN SYSTEM=ETEINDRE LE SYSTEME + + +[ERROR] +ERROR=ERREUR +NETWORK ERROR=ERREUR RESEAU +NOT FOUND=NON TROUVE + + +[INPUT] +CONFIGURE INPUT=CONFIGURER UNE MANETTE +ARE YOU SURE YOU WANT TO CONFIGURE INPUT?= ES-TU CERTAIN DE VOULOIR CONFIGURER UNE MANETTE ? + + +[SYSTEM] +SYSTEM SETTINGS=CONFIGURATION DU SYSTEME + + +[LOWERCASE] +choose=choisir +select=selectionner +close=fermer +back=retour +dark menu=menu sombre +change=changer + diff --git a/lang/gn_PY.ini b/lang/gn_PY.ini new file mode 100644 index 0000000000..5fea5c9a09 --- /dev/null +++ b/lang/gn_PY.ini @@ -0,0 +1,315 @@ +[META] +CODE=gn_PY +NAME=Guaraní + + +[MAIN] +MAIN MENU=MENÚ PRINCIPAL +QUIT=ESÊ +YES=HÉE +NO=NAHÁNIRI +OK=OK +BACK=VOLVER +START=ÑEPYRŨ +SELECT=PORAVO +CANCEL=HEJA + + +[GAME LIST] +OPTIONS=OPCIÓN KUÉRA +JUMP TO ...=HO REHO ... +LAUNCH SYSTEM SCREENSAVER=ÑEPYRŨ PANTALLA JOKY +SORT GAMES BY=EMOHENDA JUEGO KUÉRA +FILTER GAMELIST=MBOPORU LISTA JUEGO +ADD/REMOVE GAMES TO THIS GAME COLLECTION=MOĨ/PE’A JUEGO KO COLECCIÓN-GUI +FINISH EDITING=OPA MOAMBUE +COLLECTION=COLECCIÓN +COLLECTIONS=COLECCIÓN KUÉRA +ALL GAMES=OPA JUEGO +GET NEW RANDOM GAMES=ERU JUEGO ALEATORIO PYAHU +EDIT THIS FOLDER'S METADATA=MOAMBUE METADATO CARPETA +EDIT THIS GAME'S METADATA=MOAMBUE METADATO JUEGO +CLOSE=MBOTY + + +[SCRAPER] +SCRAPER=SCRAPER +SCRAPE FROM=MOOGUE JEPURU +SCRAPE RATINGS=MOOGUE CALIFICACIÓN +SCRAPE NOW=MOOGUE KO’ÁG̃A + + +[SETTINGS] +SOUND SETTINGS=ÑEÑE’Ẽ PUAPU +UI SETTINGS=INTERFAZ +OTHER SETTINGS=OPCIÓN AMBUE +GAME COLLECTION SETTINGS=COLECCIÓN JUEGO +SCREEN RESOLUTION=RESOLUCIÓN PANTALLA +ENABLE NAVIGATION SOUNDS=MYANDU PUAPU MENÚ +ENABLE VIDEO AUDIO=MYANDU PUAPU VIDEO +SYSTEM VOLUME=PUAPU SISTEMA +THEME SET=TEMA +TRANSITION STYLE=ESTILO TRANSICIÓN +CAROUSEL TRANSITIONS=TRANSICIÓN CARRUSEL +QUICK SYSTEM SELECT=PORAVO SISTEMA PYA’E +UI MODE=MODO INTERFAZ +SCREENSAVER SETTINGS=AJUSTE PANTALLA JOKY +ON-SCREEN HELP=PYTYVÕ PANTALLA PE +ENABLE FILTERS=MYANDU FILTRO +USE FULL SCREEN PAGING FOR LB/RB=PÁGINA TUICHÁVA LB/RB +START ON SYSTEM=ÑEPYRŨ SISTEMA RE +SHOW FRAMERATE=HECHA FPS +AUDIO CARD=TARJETA PUAPU +AUDIO DEVICE=PUAPU JEPURU +POWER SAVER MODES=MODO AHORRO ENERGÍA +VRAM LIMIT=LÍMITE VRAM +PARSE GAMESLISTS ONLY=LEE LISTA JUEGO AÑOITE +SEARCH FOR LOCAL ART=HEKA ARTE LOCAL +SHOW HIDDEN FILES=HECHA ARCHIVO OKA’ÝVA +INDEX FILES DURING SCREENSAVER=INDICE ARCHIVO PANTALLA JOKY PE +BACKGROUND INDEXING=INDICE FONDO PE +SAVE METADATA=ÑONGATU METADATO +IGNORE ARTICLES (NAME SORT ONLY)=ANIVE EIPURU ARTÍCULO (TÉRA AÑOITE) +DISABLE START MENU IN KID MODE=MBOTY MENÚ START MITÃ MODE +USE OMX PLAYER (HW ACCELERATED)=IPURU OMX PLAYER (HW) +DARK MENU=MENÚ YVATE’Ỹ +CLOCK=ARAVO +THEME OPTIONS=OPCIÓN TEMA + + +[LANGUAGE] +LANGUAGE=ÑE’Ẽ + + +[META] +EDIT METADATA=MOAMBUE METADATO +GAME=JUEGO +FOLDER=CARPETA +NAME=TÉRA +ENTER GAME NAME=EHAI TÉRA JUEGO +DESCRIPTION=DESCRIPCIÓN +ENTER DESCRIPTION=EHAI DESCRIPCIÓN +RELEASE DATE=ARAVO ÑEMOSÊ +ENTER RELEASE DATE=EHAI ARAVO ÑEMOSÊ +PLAY COUNT=JUEGO JEY +ENTER NUMBER OF TIMES PLAYED=EHAI MBY JUEY +LAST PLAYED=JUEGO PAHÁPE +ENTER LAST PLAYED DATE=EHAI ARAVO JUEGO PAHÁPE + +SCRAPE=MOOGUE +SAVE=ÑONGATU +CANCEL=HEJA +DELETE=MBYAI +THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?=KO’Ã OMBYAI JUEGO ARCHIVO!\nEIKUAÁPA? +SAVE CHANGES?=EÑONGATÚPA MOAMBUE? +YES=HÉE +NO=NAHÁNIRI +BACK=VOLVER +CLOSE=MBOTY + + +[VIEW] +SYSTEM INFO=INFO SISTEMA +CONFIGURATION=CONFIGURACIÓN +GAME=JUEGO +GAMES=JUEGO KUÉRA +AVAILABLE=OJEPURUVA +CHOOSE=PORAVO +SELECT=PORAVO +RANDOM=ALEATORIO +LAUNCH SCREENSAVER=ÑEPYRŨ PANTALLA JOKY + + +[GAMELIST] +GAMELIST VIEW STYLE=ESTILO VISTA +automatic=automático +basic=básico +detailed=detallado +video=video +grid=cuadrícula + + +[GAME DATA] +RATING=CALIF. +RELEASE DATE=ARAVO +DEVELOPER=MOHEÑÓI +PUBLISHER=MOHERAKUÃ +GENRE=TIPO +PLAY COUNT=JUEY +LAST PLAYED=PAHÁPE +PLAYERS=JUG. +FAVORITES=FAV. + + +[MESSAGES] +REALLY RESTART?=REINICIÁPA? +REALLY QUIT?=ESÊPA? +REALLY SHUTDOWN?=MBOTÝPA? +FILES SORTED=ARCHIVO OÑEMOHENDA +You are changing the UI to a restricted mode:=Reimoambue interfaz peteĩ modo jejoko pe: +This will hide most menu-options to prevent changes to the system.=Kóva okañymby heta opción ani hag̃ua oñemoambue sistema. +To unlock and return to the full UI, enter this code:=Rejevy hag̃ua interfaz tuicháva, emoinge ko código: +Do you want to proceed?=Eipotápa eku’e? + + +[GAME COLLECTION] +GAME COLLECTION SETTINGS=AJUSTE COLECCIÓN JUEGO +AUTOMATIC GAME COLLECTIONS=COLECCIÓN AUTOMÁTICA +CUSTOM GAME COLLECTIONS=COLECCIÓN JEIPYRE +RANDOM GAME COLLECTION SETTINGS=AJUSTE COLECCIÓN ALEATORIA +CREATE NEW CUSTOM COLLECTION FROM THEME=JAPO COLECCIÓN PYAHU TEMA GUI +CREATE NEW CUSTOM COLLECTION=JAPO COLECCIÓN PYAHU +GROUP UNTHEMED CUSTOM COLLECTIONS=ATY COLECCIÓN TEMA’Ỹ +SORT CUSTOM COLLECTIONS AND SYSTEMS=EMOHENDA COLECCIÓN HA SISTEMA +1 SELECTED=1 PORAVOPYRE +9 SELECTED=9 PORAVOPYRE +FINISH EDITING=OPA MOAMBUE +COLLECTION=COLECCIÓN +SELECT THEME FOLDER=PORAVO CARPETA TEMA +New Collection Name=Téra coleccióngui +SHOW SYSTEM NAME IN COLLECTIONS=HECHA TÉRA SISTEMA +PRESS (Y) TWICE TO REMOVE FROM FAVS./COLL.=EJOPY (Y) MOKÕI JEY EPE’A HAG̃UA +ADD/REMOVE GAMES WHILE SCREENSAVER TO=MOĨ/PE’A JUEGO PANTALLA JOKY PE += +SELECT COLLECTIONS=PORAVO COLECCIÓN KUÉRA + + +[COLLECTIONS] +all games=OPA JUEGO +last played=JUEGO RAMOITE +favorites=FAVORITO +collections=COLECCIÓN +random=ALEATORIO + + +[GAMELIST_DATA] +RELEASED=ÑEMOSÊ +LAST PLAYED=PAHÁPE +TIMES PLAYED=JUEY +PLAYERS=JUGADOR +OPTIONS=OPCIÓN +MENU=MENÚ +BACK=VOLVER +LAUNCH=ÑEPYRŨ +SYSTEM=SISTEMA +CHOOSE=PORAVO +FAVORITES=FAVORITO +RANDOM=ALEATORIO + + +[HELP] +CHOOSE=PORAVO +MENU=MENÚ +RANDOM=ALEATORIO +SELECT=PORAVO +BACK=VOLVER +OPTIONS=OPCIÓN +FAVORITE=FAVORITO +FAVORITES=FAVORITO +TOGGLE FAVORITE=MOĨ/PE’A FAVORITO +ADD/REMOVE FAVORITE=MOĨ/PE’A FAVORITO +LAUNCH SCREENSAVER=ÑEPYRŨ PANTALLA JOKY +CHANGE=MOAMBUE + + +[GENERAL] +DEFAULT=OĨHAGUÉICHA +CUSTOM=JEIPYRE +SYSTEM=SISTEMA +OPTIONS=OPCIÓN +FAVORITES=FAVORITO +NONE=AVAVE + + +[RELATIVE TIME] +DAYS_AGO=%d ára ohasa +HOURS_AGO=%d aravo ohasa +MINS_AGO=%d aravo’i ohasa +SECS_AGO=%d aravo’i michĩ ohasa +NEVER=ARAMAVE +UNKNOWN=JEIKUAÁ’Ỹ + + +[SCRAPER_MULTI] +SCRAPING IN PROGRESS=MOOGUE OJAPO HÍNA... +INPUT=EHAI +SEARCH=HEKA +SKIP=EPO +STOP=PYTA +STOP (PROGRESS SAVED)=PYTA (OÑONGATU) +GAME SCRAPED=JUEGO OÑEMBOHESA’ỸIJEVY +GAMES SCRAPED=JUEGO KUÉRA OÑEMBOHESA’ỸIJEVY +GAME SKIPPED=JUEGO OÑEPO +GAMES SKIPPED=JUEGO KUÉRA OÑEPO +NO GAMES WERE SCRAPED.=NDAIPÓI JUEGO OÑEMBOHESA’ỸIVA. +OF=GUI + + +[COLLECTION_POPUPS] +EDITING_COLLECTION=Reimoambue colección '{collection}'. Emoĩ térã epe’a juego (Y) rupive. +FINISHED_EDITING_COLLECTION=Opa moambue colección '{collection}'. +PRESS_AGAIN_TO_REMOVE=Ejopy jevy epe’a hag̃ua '{collection}'-gui. +ADDED_TO_COLLECTION=Oñemoĩ '{game}' '{collection}'-pe. +REMOVED_FROM_COLLECTION=Oñembogue '{game}' '{collection}'-gui. +EMPTY_COLLECTION=Ko colección nandi. + + +[COLLECTION_DIALOG] +SELECT ALL=PORAVOPA +SELECT NONE=ANIVE EIPORAVO + + +[DATE] +NEVER=ARAMAVE +UNKNOWN=JEIKUAÁ’Ỹ +DAYS AGO=ÁRA Ohasa +HOURS AGO=ARAVO Ohasa +MINUTES AGO=ARAVO’I Ohasa +SECONDS AGO=ARAVO’I MICHĨ Ohasa + + +[BUTTONS] +A=ACEPTAR +B=VOLVER +X=ALEATORIO +Y=FAVORITO +START=ÑEPYRŨ +SELECT=PORAVO +CHANGE=MOAMBUE + + +[SYSTEMVIEW] +ARCADE=ARCADE +RETROPIE=RETROPIE +GAMES AVAILABLE=JUEGO OJEPURUVA + + +[QUIT] +QUIT EMULATIONSTATION=ESÊ EMULATIONSTATION-GUI +RESTART EMULATIONSTATION=REINICIA EMULATIONSTATION +RESTART SYSTEM=REINICIA SISTEMA +SHUTDOWN SYSTEM=MBOTY SISTEMA + + +[ERROR] +ERROR=JEJAVY +NETWORK ERROR=JEJAVY RED +NOT FOUND=NDOJOTOPÁI + + +[INPUT] +CONFIGURE INPUT=ÑEMOĨ CONTROL +ARE YOU SURE YOU WANT TO CONFIGURE INPUT?=EIKUAÁPA EIPOTA ÑEMOĨ CONTROL? + + +[SYSTEM] +SYSTEM SETTINGS=AJUSTE SISTEMA + + +[LOWERCASE] +choose=poravo +select=poravo +close=mboty +back=guevi +dark menu=menú yvate’ỹ +change=moambue diff --git a/lang/it.ini b/lang/it.ini new file mode 100644 index 0000000000..fa1c465a05 --- /dev/null +++ b/lang/it.ini @@ -0,0 +1,315 @@ +[META] +CODE=it_IT +NAME=Italiano + + +[MAIN] +MAIN MENU=MENU PRINCIPALE +QUIT=ESCI +YES=SÌ +NO=NO +OK=OK +BACK=INDIETRO +START=AVVIO +SELECT=SELEZIONA +CANCEL=ANNULLA + + +[GAME LIST] +OPTIONS=OPZIONI +JUMP TO ...=VAI A ... +LAUNCH SYSTEM SCREENSAVER=AVVIA SALVASCHERMO DI SISTEMA +SORT GAMES BY=ORDINA GIOCHI PER +FILTER GAMELIST=FILTRA LISTA GIOCHI +ADD/REMOVE GAMES TO THIS GAME COLLECTION=AGGIUNGI/RIMUOVI GIOCHI DA QUESTA COLLEZIONE +FINISH EDITING=TERMINA MODIFICA +COLLECTION=COLLEZIONE +COLLECTIONS=COLLEZIONI +ALL GAMES=TUTTI I GIOCHI +GET NEW RANDOM GAMES=OTTIENI NUOVI GIOCHI CASUALI +EDIT THIS FOLDER'S METADATA=MODIFICA METADATI DELLA CARTELLA +EDIT THIS GAME'S METADATA=MODIFICA METADATI DEL GIOCO +CLOSE=CHIUDI + + +[SCRAPER] +SCRAPER=SCRAPER +SCRAPE FROM=FONTE DATI +SCRAPE RATINGS=OTTIENI VALUTAZIONI +SCRAPE NOW=CERCA ORA + + +[SETTINGS] +SOUND SETTINGS=IMPOSTAZIONI AUDIO +UI SETTINGS=INTERFACCIA +OTHER SETTINGS=ALTRE OPZIONI +GAME COLLECTION SETTINGS=COLLEZIONI +SCREEN RESOLUTION=RISOLUZIONE SCHERMO +ENABLE NAVIGATION SOUNDS=SUONI DI NAVIGAZIONE +ENABLE VIDEO AUDIO=AUDIO VIDEO +SYSTEM VOLUME=VOLUME DI SISTEMA +THEME SET=TEMA +TRANSITION STYLE=STILE DI TRANSIZIONE +CAROUSEL TRANSITIONS=TRANSIZIONI CAROSELLO +QUICK SYSTEM SELECT=SELEZIONE RAPIDA SISTEMA +UI MODE=MODALITÀ INTERFACCIA +SCREENSAVER SETTINGS=IMPOSTAZIONI SALVASCHERMO +ON-SCREEN HELP=AIUTO A SCHERMO +ENABLE FILTERS=ABILITA FILTRI +USE FULL SCREEN PAGING FOR LB/RB=PAGINAZIONE A SCHERMO INTERO PER LB/RB +START ON SYSTEM=AVVIA SU SISTEMA +SHOW FRAMERATE=MOSTRA FPS +AUDIO CARD=SCHEDA AUDIO +AUDIO DEVICE=DISPOSITIVO AUDIO +POWER SAVER MODES=MODALITÀ RISPARMIO ENERGIA +VRAM LIMIT=LIMITE VRAM +PARSE GAMESLISTS ONLY=LEGGI SOLO LISTE GIOCHI +SEARCH FOR LOCAL ART=CERCA GRAFICA LOCALE +SHOW HIDDEN FILES=MOSTRA FILE NASCOSTI +INDEX FILES DURING SCREENSAVER=INDICIZZA FILE DURANTE SALVASCHERMO +BACKGROUND INDEXING=INDICIZZAZIONE IN BACKGROUND +SAVE METADATA=SALVA METADATI +IGNORE ARTICLES (NAME SORT ONLY)=IGNORA ARTICOLI (SOLO ORDINAMENTO NOME) +DISABLE START MENU IN KID MODE=DISABILITA MENU START IN MODALITÀ BAMBINI +USE OMX PLAYER (HW ACCELERATED)=USA OMX PLAYER (ACCELERAZIONE HW) +DARK MENU=MENU SCURO +CLOCK=OROLOGIO +THEME OPTIONS=OPZIONI TEMA + + +[LANGUAGE] +LANGUAGE=LINGUA + + +[META] +EDIT METADATA=MODIFICA METADATI +GAME=GIOCO +FOLDER=CARTELLA +NAME=NOME +ENTER GAME NAME=NOME DEL GIOCO +DESCRIPTION=DESCRIZIONE +ENTER DESCRIPTION=INSERISCI DESCRIZIONE +RELEASE DATE=DATA DI USCITA +ENTER RELEASE DATE=INSERISCI DATA DI USCITA +PLAY COUNT=VOLTE GIOCATO +ENTER NUMBER OF TIMES PLAYED=INSERISCI NUMERO DI VOLTE GIOCATO +LAST PLAYED=ULTIMA PARTITA +ENTER LAST PLAYED DATE=INSERISCI DATA ULTIMA PARTITA + +SCRAPE=OTTIENI DATI +SAVE=SALVA +CANCEL=ANNULLA +DELETE=ELIMINA +THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?=QUESTO ELIMINERÀ I FILE DI GIOCO!\nSEI SICURO? +SAVE CHANGES?=SALVARE LE MODIFICHE? +YES=SÌ +NO=NO +BACK=INDIETRO +CLOSE=CHIUDI + + +[VIEW] +SYSTEM INFO=INFO SISTEMA +CONFIGURATION=CONFIGURAZIONE +GAME=GIOCO +GAMES=GIOCHI +AVAILABLE=DISPONIBILI +CHOOSE=SCEGLI +SELECT=SELEZIONA +RANDOM=CASUALE +LAUNCH SCREENSAVER=AVVIA SALVASCHERMO + + +[GAMELIST] +GAMELIST VIEW STYLE=STILE VISUALIZZAZIONE +automatic=automatico +basic=base +detailed=dettagliato +video=video +grid=griglia + + +[GAME DATA] +RATING=VALUTAZIONE +RELEASE DATE=DATA +DEVELOPER=SVIL. +PUBLISHER=DISTR. +GENRE=GEN. +PLAY COUNT=VOLTE +LAST PLAYED=ULT. VOLTA +PLAYERS=GIOC. +FAVORITES=PREF. + + +[MESSAGES] +REALLY RESTART?=RIAVVIARE? +REALLY QUIT?=USCIRE? +REALLY SHUTDOWN?=SPEGNERE? +FILES SORTED=FILE ORDINATI +You are changing the UI to a restricted mode:=Stai cambiando l’interfaccia in modalità limitata: +This will hide most menu-options to prevent changes to the system.=Questo nasconderà la maggior parte delle opzioni per evitare modifiche al sistema. +To unlock and return to the full UI, enter this code:=Per sbloccare e tornare all’interfaccia completa, inserisci questo codice: +Do you want to proceed?=Vuoi continuare? + + +[GAME COLLECTION] +GAME COLLECTION SETTINGS=IMPOSTAZIONI COLLEZIONI +AUTOMATIC GAME COLLECTIONS=COLLEZIONI AUTOMATICHE +CUSTOM GAME COLLECTIONS=COLLEZIONI PERSONALIZZATE +RANDOM GAME COLLECTION SETTINGS=IMPOSTAZIONI COLLEZIONE CASUALE +CREATE NEW CUSTOM COLLECTION FROM THEME=CREA COLLEZIONE DAL TEMA +CREATE NEW CUSTOM COLLECTION=CREA NUOVA COLLEZIONE +GROUP UNTHEMED CUSTOM COLLECTIONS=RAGGRUPPA COLLEZIONI SENZA TEMA +SORT CUSTOM COLLECTIONS AND SYSTEMS=ORDINA COLLEZIONI E SISTEMI +1 SELECTED=1 SELEZIONATA +9 SELECTED=9 SELEZIONATE +FINISH EDITING=TERMINA MODIFICA +COLLECTION=COLLEZIONE +SELECT THEME FOLDER=SELEZIONA CARTELLA TEMA +New Collection Name=Nome della collezione +SHOW SYSTEM NAME IN COLLECTIONS=MOSTRA NOME SISTEMA +PRESS (Y) TWICE TO REMOVE FROM FAVS./COLL.=PREMI (Y) DUE VOLTE PER RIMUOVERE DA PREF./COLL. +ADD/REMOVE GAMES WHILE SCREENSAVER TO=AGGIUNGI/RIMUOVI GIOCHI NEL SALVASCHERMO A += +SELECT COLLECTIONS=SELEZIONA COLLEZIONI + + +[COLLECTIONS] +all games=TUTTI I GIOCHI +last played=ULTIMI GIOCATI +favorites=PREFERITI +collections=COLLEZIONI +random=CASUALI + + +[GAMELIST_DATA] +RELEASED=USCITA +LAST PLAYED=ULT. VOLTA +TIMES PLAYED=VOLTE +PLAYERS=GIOCATORI +OPTIONS=OPZIONI +MENU=MENU +BACK=INDIETRO +LAUNCH=AVVIA +SYSTEM=SISTEMA +CHOOSE=SCEGLI +FAVORITES=PREFERITI +RANDOM=CASUALE + + +[HELP] +CHOOSE=SCEGLI +MENU=MENU +RANDOM=CASUALE +SELECT=SELEZIONA +BACK=INDIETRO +OPTIONS=OPZIONI +FAVORITE=PREFERITO +FAVORITES=PREFERITI +TOGGLE FAVORITE=AGGIUNGI/RIMUOVI PREFERITO +ADD/REMOVE FAVORITE=AGGIUNGI/RIMUOVI PREFERITO +LAUNCH SCREENSAVER=ATTIVA SALVASCHERMO +CHANGE=CAMBIA + + +[GENERAL] +DEFAULT=PREDEFINITO +CUSTOM=PERSONALIZZATO +SYSTEM=SISTEMA +OPTIONS=OPZIONI +FAVORITES=PREFERITI +NONE=NESSUNO + + +[RELATIVE TIME] +DAYS_AGO=%d giorni fa +HOURS_AGO=%d ore fa +MINS_AGO=%d min fa +SECS_AGO=%d sec fa +NEVER=MAI +UNKNOWN=SCONOSCIUTO + + +[SCRAPER_MULTI] +SCRAPING IN PROGRESS=RECUPERO DATI IN CORSO... +INPUT=INSERISCI TESTO +SEARCH=CERCA +SKIP=SALTA +STOP=FERMA +STOP (PROGRESS SAVED)=FERMA (PROGRESSO SALVATO) +GAME SCRAPED=GIOCO AGGIORNATO +GAMES SCRAPED=GIOCHI AGGIORNATI +GAME SKIPPED=GIOCO SALTATO +GAMES SKIPPED=GIOCHI SALTATI +NO GAMES WERE SCRAPED.=NESSUN GIOCO È STATO AGGIORNATO. +OF=DI + + +[COLLECTION_POPUPS] +EDITING_COLLECTION=Modifica della collezione '{collection}'. Aggiungi o rimuovi giochi con (Y). +FINISHED_EDITING_COLLECTION=Modifica della collezione '{collection}' completata. +PRESS_AGAIN_TO_REMOVE=Premi di nuovo per rimuovere da '{collection}'. +ADDED_TO_COLLECTION=Aggiunto '{game}' a '{collection}'. +REMOVED_FROM_COLLECTION=Rimosso '{game}' da '{collection}'. +EMPTY_COLLECTION=Questa collezione è vuota. + + +[COLLECTION_DIALOG] +SELECT ALL=SELEZIONA TUTTO +SELECT NONE=DESELEZIONA TUTTO + + +[DATE] +NEVER=MAI +UNKNOWN=SCONOSCIUTO +DAYS AGO=GIORNI FA +HOURS AGO=ORE FA +MINUTES AGO=MINUTI FA +SECONDS AGO=SECONDI FA + + +[BUTTONS] +A=ACCETTA +B=INDIETRO +X=CASUALE +Y=PREFERITI +START=AVVIO +SELECT=SELEZIONA +CHANGE=CAMBIA + + +[SYSTEMVIEW] +ARCADE=ARCADE +RETROPIE=RETROPIE +GAMES AVAILABLE=GIOCHI DISPONIBILI + + +[QUIT] +QUIT EMULATIONSTATION=ESCI DA EMULATIONSTATION +RESTART EMULATIONSTATION=RIAVVIA EMULATIONSTATION +RESTART SYSTEM=RIAVVIA SISTEMA +SHUTDOWN SYSTEM=SPEGNI SISTEMA + + +[ERROR] +ERROR=ERRORE +NETWORK ERROR=ERRORE DI RETE +NOT FOUND=NON TROVATO + + +[INPUT] +CONFIGURE INPUT=CONFIGURA CONTROLLI +ARE YOU SURE YOU WANT TO CONFIGURE INPUT?=SEI SICURO DI VOLER CONFIGURARE I CONTROLLI? + + +[SYSTEM] +SYSTEM SETTINGS=IMPOSTAZIONI DI SISTEMA + + +[LOWERCASE] +choose=scegli +select=seleziona +close=chiudi +back=indietro +dark menu=menu scuro +change=cambia diff --git "a/lang/ja (\346\227\245\346\234\254\350\252\236).ini" "b/lang/ja (\346\227\245\346\234\254\350\252\236).ini" new file mode 100644 index 0000000000..4dbae37171 --- /dev/null +++ "b/lang/ja (\346\227\245\346\234\254\350\252\236).ini" @@ -0,0 +1,248 @@ +[META] +CODE=ja +NAME=日本語 + +[MAIN] +MAIN MENU=メインメニュー +QUIT=終了 +YES=はい +NO=いいえ +OK=OK +BACK=戻る +START=開始 +SELECT=選択 +CANCEL=キャンセル + +[GAME LIST] +OPTIONS=オプション +JUMP TO ...=移動... +LAUNCH SYSTEM SCREENSAVER=スクリーンセーバー開始 +SORT GAMES BY=並び替え +FILTER GAMELIST=フィルター +ADD/REMOVE GAMES TO THIS GAME COLLECTION=コレクションに追加/削除 +FINISH EDITING=編集終了 +COLLECTION=コレクション +COLLECTIONS=コレクション一覧 +ALL GAMES=すべてのゲーム +GET NEW RANDOM GAMES=ランダムゲーム取得 +EDIT THIS FOLDER'S METADATA=フォルダ情報編集 +EDIT THIS GAME'S METADATA=ゲーム情報編集 +CLOSE=閉じる + +[SCRAPER] +SCRAPER=スクレーパー +SCRAPE FROM=取得元 +SCRAPE RATINGS=評価取得 +SCRAPE NOW=今すぐ取得 + +[SETTINGS] +SOUND SETTINGS=サウンド +UI SETTINGS=UI設定 +OTHER SETTINGS=その他 +GAME COLLECTION SETTINGS=コレクション設定 +SCREEN RESOLUTION=解像度 +ENABLE NAVIGATION SOUNDS=操作音 +ENABLE VIDEO AUDIO=映像音声 +SYSTEM VOLUME=音量 +THEME SET=テーマ +TRANSITION STYLE=切替スタイル +CAROUSEL TRANSITIONS=カルーセル切替 +QUICK SYSTEM SELECT=高速システム選択 +UI MODE=UIモード +SCREENSAVER SETTINGS=スクリーンセーバー +ON-SCREEN HELP=画面ヘルプ +ENABLE FILTERS=フィルター有効 +USE FULL SCREEN PAGING FOR LB/RB=全画面ページ切替 LB/RB +START ON SYSTEM=起動システム +SHOW FRAMERATE=FPS表示 +AUDIO CARD=オーディオカード +AUDIO DEVICE=オーディオデバイス +POWER SAVER MODES=省電力モード +VRAM LIMIT=VRAM上限 +PARSE GAMESLISTS ONLY=ゲームリストのみ解析 +SEARCH FOR LOCAL ART=ローカルアート検索 +SHOW HIDDEN FILES=隠しファイル表示 +INDEX FILES DURING SCREENSAVER=セーバー中にインデックス +SAVE METADATA=メタ保存 +IGNORE ARTICLES (NAME SORT ONLY)=記事無視(並び替え) +DISABLE START MENU IN KID MODE=キッズモードでスタート非表示 +USE OMX PLAYER (HW ACCELERATED)=OMX再生(HW) +BACKGROUND INDEXING=バックグラウンド登録 + +[LANGUAGE] +LANGUAGE=言語 + +[META] +EDIT METADATA=メタ編集 +GAME=ゲーム +FOLDER=フォルダ +NAME=名前 +ENTER GAME NAME=ゲーム名入力 +DESCRIPTION=説明 +ENTER DESCRIPTION=説明入力 +RELEASE DATE=発売日 +ENTER RELEASE DATE=日付入力 +PLAY COUNT=プレイ回数 +ENTER NUMBER OF TIMES PLAYED=回数入力 +LAST PLAYED=最終プレイ +ENTER LAST PLAYED DATE=日付入力 + +SCRAPE=データ取得 +SAVE=保存 +CANCEL=キャンセル +DELETE=削除 +THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?=ゲームファイルを削除します!\nよろしいですか? +SAVE CHANGES?=変更を保存? +YES=はい +NO=いいえ +BACK=戻る +CLOSE=閉じる + +[VIEW] +SYSTEM INFO=システム情報 +CONFIGURATION=設定 +GAME=ゲーム +GAMES=ゲーム +AVAILABLE=利用可能 +CHOOSE=選択 +SELECT=選択 +RANDOM=ランダム +LAUNCH SCREENSAVER=スクリーンセーバー開始 + +[GAMELIST] +GAMELIST VIEW STYLE=表示スタイル +automatic=自動 +basic=基本 +detailed=詳細 +video=ビデオ +grid=グリッド + +[GAME DATA] +RATING=評価 +RELEASE DATE=発売日 +DEVELOPER=開発元 +PUBLISHER=販売元 +GENRE=ジャンル +PLAY COUNT=回数 +LAST PLAYED=最終 +PLAYERS=人数 +FAVORITES=お気に入り + +[MESSAGES] +REALLY RESTART?=再起動しますか? +REALLY QUIT?=終了しますか? +REALLY SHUTDOWN?=電源を切りますか? +FILES SORTED=ファイルを並び替えました +You are changing the UI to a restricted mode:=制限モードに切替中: +This will hide most menu-options to prevent changes to the system.=安全のため、メニュー項目を非表示にします +To unlock and return to the full UI, enter this code:= +フルUIに戻るにはコード入力: +Do you want to proceed?=続行しますか? + +[GAME COLLECTION] +GAME COLLECTION SETTINGS=コレクション設定 +AUTOMATIC GAME COLLECTIONS=自動コレクション +CUSTOM GAME COLLECTIONS=カスタムコレクション +RANDOM GAME COLLECTION SETTINGS=ランダムコレクション設定 +CREATE NEW CUSTOM COLLECTION FROM THEME=テーマで新規コレクション +CREATE NEW CUSTOM COLLECTION=新規カスタムコレクション +GROUP UNTHEMED CUSTOM COLLECTIONS=テーマなしをまとめる +SORT CUSTOM COLLECTIONS AND SYSTEMS=コレクション並び替え +BACK=戻る +1 SELECTED=1 選択 +9 SELECTED=9 選択 +FINISH EDITING=編集完了 +COLLECTION=コレクション +SELECT THEME FOLDER=テーマフォルダ選択 +New Collection Name=名前入力 +SHOW SYSTEM NAME IN COLLECTIONS=システム名表示 +PRESS (Y) TWICE TO REMOVE FROM FAVS./COLL.=Y2回で削除 +ADD/REMOVE GAMES WHILE SCREENSAVER TO=セーバー中に追加/削除 +=デフォルト +SELECT COLLECTIONS=コレクション選択 + +[COLLECTIONS] +all games=すべてのゲーム +last played=最近のプレイ +favorites=お気に入り +collections=コレクション一覧 +random=ランダム + +[GAMELIST_DATA] +RELEASED=発売日 +LAST PLAYED=最終プレイ +TIMES PLAYED=回数 +PLAYERS=人数 +OPTIONS=オプション +MENU=メニュー +BACK=戻る +LAUNCH=開始 +SYSTEM=システム +CHOOSE=選択 +FAVORITES=お気に入り +RANDOM=ランダム + +[HELP] +CHOOSE=選択 +MENU=メニュー +RANDOM=ランダム +SELECT=選択 +BACK=戻る +OPTIONS=オプション +FAVORITE=お気に入り +FAVORITES=お気に入り一覧 +TOGGLE FAVORITE=お気に入り切替 +ADD/REMOVE FAVORITE=お気に入り追加/削除 +LAUNCH SCREENSAVER=セーバー開始 + +[GENERAL] +DEFAULT=デフォルト +CUSTOM=カスタム +SYSTEM=システム +OPTIONS=オプション +FAVORITES=お気に入り +NONE=なし + +[RELATIVE TIME] +DAYS_AGO=%d 日前 +HOURS_AGO=%d 時間前 +MINS_AGO=%d 分前 +SECS_AGO=%d 秒前 +NEVER=なし +UNKNOWN=不明 + +[BUTTONS] +A=OK +B=戻る +X=ランダム +Y=お気に入り +START=開始 +SELECT=選択 + +[SYSTEMVIEW] +ARCADE=アーケード +RETROPIE=レトロパイ +GAMES AVAILABLE=利用可能ゲーム + +[QUIT] +QUIT EMULATIONSTATION=終了 +RESTART EMULATIONSTATION=再起動 +RESTART SYSTEM=システム再起動 +SHUTDOWN SYSTEM=電源オフ + +[ERROR] +ERROR=エラー +NETWORK ERROR=ネットワークエラー +NOT FOUND=見つかりません + +[INPUT] +CONFIGURE INPUT=入力設定 +ARE YOU SURE YOU WANT TO CONFIGURE INPUT?=入力設定しますか? + +[SYSTEM] +SYSTEM SETTINGS=システム設定 + +[LOWERCASE] +choose=選択 +select=選択 +close=閉じる diff --git a/lang/pt.ini b/lang/pt.ini new file mode 100644 index 0000000000..6c35460d5e --- /dev/null +++ b/lang/pt.ini @@ -0,0 +1,315 @@ +[META] +CODE=pt_BR +NAME=Português (Brasil) + + +[MAIN] +MAIN MENU=MENU PRINCIPAL +QUIT=SAIR +YES=SIM +NO=NÃO +OK=OK +BACK=VOLTAR +START=INICIAR +SELECT=SELECIONAR +CANCEL=CANCELAR + + +[GAME LIST] +OPTIONS=OPÇÕES +JUMP TO ...=IR PARA ... +LAUNCH SYSTEM SCREENSAVER=INICIAR PROTETOR DE TELA DO SISTEMA +SORT GAMES BY=ORDENAR JOGOS POR +FILTER GAMELIST=FILTRAR LISTA DE JOGOS +ADD/REMOVE GAMES TO THIS GAME COLLECTION=ADICIONAR/REMOVER JOGOS DESTA COLEÇÃO +FINISH EDITING=FINALIZAR EDIÇÃO +COLLECTION=COLEÇÃO +COLLECTIONS=COLEÇÕES +ALL GAMES=TODOS OS JOGOS +GET NEW RANDOM GAMES=OBTER NOVOS JOGOS ALEATÓRIOS +EDIT THIS FOLDER'S METADATA=EDITAR METADADOS DA PASTA +EDIT THIS GAME'S METADATA=EDITAR METADADOS DO JOGO +CLOSE=FECHAR + + +[SCRAPER] +SCRAPER=SCRAPER +SCRAPE FROM=FONTE DE DADOS +SCRAPE RATINGS=OBTER AVALIAÇÕES +SCRAPE NOW=BUSCAR AGORA + + +[SETTINGS] +SOUND SETTINGS=CONFIGURAÇÕES DE SOM +UI SETTINGS=INTERFACE +OTHER SETTINGS=OUTRAS OPÇÕES +GAME COLLECTION SETTINGS=COLEÇÕES +SCREEN RESOLUTION=RESOLUÇÃO DE TELA +ENABLE NAVIGATION SOUNDS=SONS DE NAVEGAÇÃO +ENABLE VIDEO AUDIO=ÁUDIO DO VÍDEO +SYSTEM VOLUME=VOLUME DO SISTEMA +THEME SET=TEMA +TRANSITION STYLE=ESTILO DE TRANSIÇÃO +CAROUSEL TRANSITIONS=TRANSIÇÕES DO CARROSSEL +QUICK SYSTEM SELECT=SELEÇÃO RÁPIDA DE SISTEMA +UI MODE=MODO DE INTERFACE +SCREENSAVER SETTINGS=CONFIGURAÇÕES DO PROTETOR DE TELA +ON-SCREEN HELP=AJUDA NA TELA +ENABLE FILTERS=HABILITAR FILTROS +USE FULL SCREEN PAGING FOR LB/RB=PAGINAÇÃO EM TELA CHEIA PARA LB/RB +START ON SYSTEM=INICIAR NO SISTEMA +SHOW FRAMERATE=MOSTRAR FPS +AUDIO CARD=PLACA DE ÁUDIO +AUDIO DEVICE=DISPOSITIVO DE ÁUDIO +POWER SAVER MODES=MODOS DE ECONOMIA DE ENERGIA +VRAM LIMIT=LIMITE DE VRAM +PARSE GAMESLISTS ONLY=LER APENAS LISTAS DE JOGOS +SEARCH FOR LOCAL ART=BUSCAR ARTES LOCAIS +SHOW HIDDEN FILES=MOSTRAR ARQUIVOS OCULTOS +INDEX FILES DURING SCREENSAVER=INDEXAR ARQUIVOS NO PROTETOR DE TELA +BACKGROUND INDEXING=INDEXAÇÃO EM SEGUNDO PLANO +SAVE METADATA=SALVAR METADADOS +IGNORE ARTICLES (NAME SORT ONLY)=IGNORAR ARTIGOS (ORDENAR POR NOME) +DISABLE START MENU IN KID MODE=OCULTAR MENU START NO MODO INFANTIL +USE OMX PLAYER (HW ACCELERATED)=USAR OMX PLAYER (ACELERAÇÃO HW) +DARK MENU=MENU ESCURO +CLOCK=RELÓGIO +THEME OPTIONS=OPÇÕES DO TEMA + + +[LANGUAGE] +LANGUAGE=IDIOMA + + +[META] +EDIT METADATA=EDITAR METADADOS +GAME=JOGO +FOLDER=PASTA +NAME=NOME +ENTER GAME NAME=NOME DO JOGO +DESCRIPTION=DESCRIÇÃO +ENTER DESCRIPTION=INSIRA UMA DESCRIÇÃO +RELEASE DATE=DATA DE LANÇAMENTO +ENTER RELEASE DATE=INSIRA A DATA DE LANÇAMENTO +PLAY COUNT=VEZES JOGADO +ENTER NUMBER OF TIMES PLAYED=INSIRA QUANTAS VEZES JOGOU +LAST PLAYED=ÚLTIMA VEZ JOGADO +ENTER LAST PLAYED DATE=INSIRA A DATA DA ÚLTIMA VEZ + +SCRAPE=OBTER DADOS +SAVE=SALVAR +CANCEL=CANCELAR +DELETE=EXCLUIR +THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?=ISTO EXCLUIRÁ O(S) ARQUIVO(S) DO JOGO!\nTEM CERTEZA? +SAVE CHANGES?=SALVAR ALTERAÇÕES? +YES=SIM +NO=NÃO +BACK=VOLTAR +CLOSE=FECHAR + + +[VIEW] +SYSTEM INFO=INFORMAÇÕES DO SISTEMA +CONFIGURATION=CONFIGURAÇÃO +GAME=JOGO +GAMES=JOGOS +AVAILABLE=DISPONÍVEIS +CHOOSE=ESCOLHER +SELECT=SELECIONAR +RANDOM=ALEATÓRIO +LAUNCH SCREENSAVER=INICIAR PROTETOR DE TELA + + +[GAMELIST] +GAMELIST VIEW STYLE=ESTILO DE VISUALIZAÇÃO +automatic=automático +basic=básico +detailed=detalhado +video=vídeo +grid=grade + + +[GAME DATA] +RATING=AVALIAÇÃO +RELEASE DATE=DATA +DEVELOPER=DEV. +PUBLISHER=DIST. +GENRE=GÊN. +PLAY COUNT=VEZES +LAST PLAYED=ÚLT. VEZ +PLAYERS=JOG. +FAVORITES=FAVS. + + +[MESSAGES] +REALLY RESTART?=REINICIAR? +REALLY QUIT?=SAIR? +REALLY SHUTDOWN?=DESLIGAR? +FILES SORTED=ARQUIVOS ORDENADOS +You are changing the UI to a restricted mode:=Você está mudando a interface para o modo restrito: +This will hide most menu-options to prevent changes to the system.=Isso ocultará a maioria das opções para evitar alterações no sistema. +To unlock and return to the full UI, enter this code:=Para desbloquear e voltar à interface completa, digite este código: +Do you want to proceed?=Deseja continuar? + + +[GAME COLLECTION] +GAME COLLECTION SETTINGS=CONFIGURAÇÕES DE COLEÇÕES +AUTOMATIC GAME COLLECTIONS=COLEÇÕES AUTOMÁTICAS +CUSTOM GAME COLLECTIONS=COLEÇÕES PERSONALIZADAS +RANDOM GAME COLLECTION SETTINGS=CONFIGURAÇÕES DE COLEÇÃO ALEATÓRIA +CREATE NEW CUSTOM COLLECTION FROM THEME=CRIAR COLEÇÃO A PARTIR DO TEMA +CREATE NEW CUSTOM COLLECTION=CRIAR NOVA COLEÇÃO +GROUP UNTHEMED CUSTOM COLLECTIONS=AGRUPAR COLEÇÕES SEM TEMA +SORT CUSTOM COLLECTIONS AND SYSTEMS=ORDENAR COLEÇÕES E SISTEMAS +1 SELECTED=1 SELECIONADA +9 SELECTED=9 SELECIONADAS +FINISH EDITING=FINALIZAR EDIÇÃO +COLLECTION=COLEÇÃO +SELECT THEME FOLDER=SELECIONAR PASTA DO TEMA +New Collection Name=Nome da coleção +SHOW SYSTEM NAME IN COLLECTIONS=MOSTRAR NOME DO SISTEMA +PRESS (Y) TWICE TO REMOVE FROM FAVS./COLL.=PRESSIONE (Y) DUAS VEZES PARA REMOVER DOS FAVS./COL. +ADD/REMOVE GAMES WHILE SCREENSAVER TO=ADICIONAR/REMOVER JOGOS NO PROTETOR DE TELA PARA += +SELECT COLLECTIONS=SELECIONAR COLEÇÕES + + +[COLLECTIONS] +all games=TODOS OS JOGOS +last played=ÚLTIMOS JOGADOS +favorites=FAVORITOS +collections=COLEÇÕES +random=ALEATÓRIOS + + +[GAMELIST_DATA] +RELEASED=LANÇAMENTO +LAST PLAYED=ÚLT. VEZ +TIMES PLAYED=VEZES +PLAYERS=JOGADORES +OPTIONS=OPÇÕES +MENU=MENU +BACK=VOLTAR +LAUNCH=INICIAR +SYSTEM=SISTEMA +CHOOSE=ESCOLHER +FAVORITES=FAVORITOS +RANDOM=ALEATÓRIO + + +[HELP] +CHOOSE=ESCOLHER +MENU=MENU +RANDOM=ALEATÓRIO +SELECT=SELECIONAR +BACK=VOLTAR +OPTIONS=OPÇÕES +FAVORITE=FAVORITO +FAVORITES=FAVORITOS +TOGGLE FAVORITE=MARCAR/DESMARCAR FAVORITO +ADD/REMOVE FAVORITE=ADICIONAR/REMOVER FAVORITO +LAUNCH SCREENSAVER=ATIVAR PROTETOR DE TELA +CHANGE=ALTERAR + + +[GENERAL] +DEFAULT=PADRÃO +CUSTOM=PERSONALIZADO +SYSTEM=SISTEMA +OPTIONS=OPÇÕES +FAVORITES=FAVORITOS +NONE=NENHUM + + +[RELATIVE TIME] +DAYS_AGO=%d dias atrás +HOURS_AGO=%d horas atrás +MINS_AGO=%d min atrás +SECS_AGO=%d seg atrás +NEVER=NUNCA +UNKNOWN=DESCONHECIDO + + +[SCRAPER_MULTI] +SCRAPING IN PROGRESS=OBTENDO DADOS... +INPUT=ENTRAR TEXTO +SEARCH=BUSCAR +SKIP=IGNORAR +STOP=PARAR +STOP (PROGRESS SAVED)=PARAR (PROGRESSO SALVO) +GAME SCRAPED=JOGO ATUALIZADO +GAMES SCRAPED=JOGOS ATUALIZADOS +GAME SKIPPED=JOGO IGNORADO +GAMES SKIPPED=JOGOS IGNORADOS +NO GAMES WERE SCRAPED.=NENHUM JOGO FOI ATUALIZADO. +OF=DE + + +[COLLECTION_POPUPS] +EDITING_COLLECTION=Editando a coleção '{collection}'. Adicione ou remova jogos com (Y). +FINISHED_EDITING_COLLECTION=Edição da coleção '{collection}' finalizada. +PRESS_AGAIN_TO_REMOVE=Pressione novamente para remover de '{collection}'. +ADDED_TO_COLLECTION=Adicionado '{game}' à '{collection}'. +REMOVED_FROM_COLLECTION=Removido '{game}' de '{collection}'. +EMPTY_COLLECTION=Esta coleção está vazia. + + +[COLLECTION_DIALOG] +SELECT ALL=SELECIONAR TUDO +SELECT NONE=DESMARCAR TUDO + + +[DATE] +NEVER=NUNCA +UNKNOWN=DESCONHECIDO +DAYS AGO=DIAS ATRÁS +HOURS AGO=HORAS ATRÁS +MINUTES AGO=MINUTOS ATRÁS +SECONDS AGO=SEGUNDOS ATRÁS + + +[BUTTONS] +A=ACEITAR +B=VOLTAR +X=ALEATÓRIO +Y=FAVORITOS +START=INICIAR +SELECT=SELECIONAR +CHANGE=ALTERAR + + +[SYSTEMVIEW] +ARCADE=ARCADE +RETROPIE=RETROPIE +GAMES AVAILABLE=JOGOS DISPONÍVEIS + + +[QUIT] +QUIT EMULATIONSTATION=SAIR DO EMULATIONSTATION +RESTART EMULATIONSTATION=REINICIAR EMULATIONSTATION +RESTART SYSTEM=REINICIAR SISTEMA +SHUTDOWN SYSTEM=DESLIGAR SISTEMA + + +[ERROR] +ERROR=ERRO +NETWORK ERROR=ERRO DE REDE +NOT FOUND=NÃO ENCONTRADO + + +[INPUT] +CONFIGURE INPUT=CONFIGURAR CONTROLES +ARE YOU SURE YOU WANT TO CONFIGURE INPUT?=TEM CERTEZA QUE DESEJA CONFIGURAR OS CONTROLES? + + +[SYSTEM] +SYSTEM SETTINGS=CONFIGURAÇÕES DO SISTEMA + + +[LOWERCASE] +choose=escolher +select=selecionar +close=fechar +back=voltar +dark menu=menu escuro +change=alterar diff --git a/resources/1splash.svg b/resources/1splash.svg new file mode 100644 index 0000000000..3e6d23540b --- /dev/null +++ b/resources/1splash.svg @@ -0,0 +1,5114 @@ + + + + + + + + + + diff --git a/resources/ESX_Guide.pdf b/resources/ESX_Guide.pdf new file mode 100644 index 0000000000..49c670b81c Binary files /dev/null and b/resources/ESX_Guide.pdf differ diff --git a/resources/frame.png b/resources/frame.png index 0bca559ef1..5dfeb889d9 100644 Binary files a/resources/frame.png and b/resources/frame.png differ diff --git a/resources/frame_dark.png b/resources/frame_dark.png new file mode 100644 index 0000000000..034de980c8 Binary files /dev/null and b/resources/frame_dark.png differ diff --git a/resources/highlight_blue.png b/resources/highlight_blue.png new file mode 100644 index 0000000000..b65c0e55f0 Binary files /dev/null and b/resources/highlight_blue.png differ diff --git a/resources/highlight_dark.png b/resources/highlight_dark.png new file mode 100644 index 0000000000..28f3f7fd4e Binary files /dev/null and b/resources/highlight_dark.png differ diff --git a/resources/splash.png b/resources/splash.png new file mode 100644 index 0000000000..a6da14d41d Binary files /dev/null and b/resources/splash.png differ diff --git a/resources/splash.svg b/resources/splash.svg index b15cdec4e2..9bdf04ffde 100644 --- a/resources/splash.svg +++ b/resources/splash.svg @@ -1,55 +1,219 @@ - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - + + + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - \ No newline at end of file + +-X