diff --git a/projects/frontend/es-app/src/web/server/IRouter.h b/projects/frontend/es-app/src/web/server/IRouter.h index cc58f4feb2b27239bd1ba1110b35568f047a618a..6dc6c01f71f3900f7756298c997766c9f3d11c88 100644 --- a/projects/frontend/es-app/src/web/server/IRouter.h +++ b/projects/frontend/es-app/src/web/server/IRouter.h @@ -229,6 +229,27 @@ class IRouter */ virtual void MediaGetScreenshot(const Rest::Request& request, Http::ResponseWriter response) = 0; + /*! + * @brief Handle GET to get all roms + * @param request Request object + * @param response Response object + */ + virtual void RomsGetAll(const Rest::Request& request, Http::ResponseWriter response) = 0; + + /*! + * @brief Handle GET to get system roms + * @param request Request object + * @param response Response object + */ + virtual void RomsGetList(const Rest::Request& request, Http::ResponseWriter response) = 0; + + /*! + * @brief Handle DELETE to delete rom + * @param request Request object + * @param response Response object + */ + virtual void RomsDelete(const Rest::Request& request, Http::ResponseWriter response) = 0; + public: /*! * @brief Constructor. Set all routes @@ -271,6 +292,9 @@ class IRouter Rest::Routes::Post(mRouter, "/api/media/takescreenshot", Rest::Routes::bind(&IRouter::MediaTakeScreenshot, this)); Rest::Routes::Get(mRouter, "/api/media/*", Rest::Routes::bind(&IRouter::MediaGet, this)); Rest::Routes::Get(mRouter, "/api/media/screenshot/*", Rest::Routes::bind(&IRouter::MediaGetScreenshot, this)); + // Roms + Rest::Routes::Get(mRouter, "/api/systems/*/roms", Rest::Routes::bind(&IRouter::RomsGetList, this)); + Rest::Routes::Delete(mRouter, "/api/systems/*/roms", Rest::Routes::bind(&IRouter::RomsDelete, this)); // Default file service Rest::Routes::NotFound(mRouter, Rest::Routes::bind(&IRouter::FileServer, this)); diff --git a/projects/frontend/es-app/src/web/server/handlers/Mime.cpp b/projects/frontend/es-app/src/web/server/handlers/Mime.cpp index c45e296c1466786b4df59067960a0a2676a3816e..8de64c70298716e16cc74630f573a478e8af43d6 100644 --- a/projects/frontend/es-app/src/web/server/handlers/Mime.cpp +++ b/projects/frontend/es-app/src/web/server/handlers/Mime.cpp @@ -19,6 +19,7 @@ Pistache::Http::Mime::MediaType Mime::ImageSvg(Pistache::Http::Mime::Type::Image Pistache::Http::Mime::MediaType Mime::VideoMkv(Pistache::Http::Mime::Type::Video, Pistache::Http::Mime::Subtype::Mkv); Pistache::Http::Mime::MediaType Mime::VideoMp4(Pistache::Http::Mime::Type::Video, Pistache::Http::Mime::Subtype::Mp4); Pistache::Http::Mime::MediaType Mime::VideoAvi(Pistache::Http::Mime::Type::Video, Pistache::Http::Mime::Subtype::Avi); +Pistache::Http::Mime::MediaType Mime::VideoWebm(Pistache::Http::Mime::Type::Video, Pistache::Http::Mime::Subtype::Webm); Pistache::Http::Mime::MediaType Mime::Zip(Pistache::Http::Mime::Type::Application, Pistache::Http::Mime::Subtype::OctetStream, Pistache::Http::Mime::Suffix::Zip); Pistache::Http::Mime::MediaType Mime::FontTtf(Pistache::Http::Mime::Type::Font, Pistache::Http::Mime::Subtype::Ttf); Pistache::Http::Mime::MediaType Mime::FontWoff(Pistache::Http::Mime::Type::Font, Pistache::Http::Mime::Subtype::Woff); diff --git a/projects/frontend/es-app/src/web/server/handlers/Mime.h b/projects/frontend/es-app/src/web/server/handlers/Mime.h index 5e88d76128b03877c4d417ef9ad58da15695fe95..a5f56ae8d6ac429139f27d6dcc47599fa6b75899 100644 --- a/projects/frontend/es-app/src/web/server/handlers/Mime.h +++ b/projects/frontend/es-app/src/web/server/handlers/Mime.h @@ -38,6 +38,8 @@ class Mime static Pistache::Http::Mime::MediaType VideoMp4; //! AVI MIME Type static Pistache::Http::Mime::MediaType VideoAvi; + //! WEBM MIME Type + static Pistache::Http::Mime::MediaType VideoWebm; //! PDF MIME Type static Pistache::Http::Mime::MediaType FilePdf; //! ttf MIME Type diff --git a/projects/frontend/es-app/src/web/server/handlers/RequestHandler.cpp b/projects/frontend/es-app/src/web/server/handlers/RequestHandler.cpp index 0f099f4619181e37443047b2e1671754853d7af8..7e90f9553f64b21494e2cb5460a1818025668852 100644 --- a/projects/frontend/es-app/src/web/server/handlers/RequestHandler.cpp +++ b/projects/frontend/es-app/src/web/server/handlers/RequestHandler.cpp @@ -503,6 +503,7 @@ void RequestHandler::MediaGet(const Rest::Request& request, Http::ResponseWriter else if (ext == ".mkv") RequestHandlerTools::SendResource(path, response, Mime::VideoMkv); else if (ext == ".mp4") RequestHandlerTools::SendResource(path, response, Mime::VideoMp4); else if (ext == ".avi") RequestHandlerTools::SendResource(path, response, Mime::VideoAvi); + else if (ext == ".webm") RequestHandlerTools::SendResource(path, response, Mime::VideoWebm); // Unknown else RequestHandlerTools::Send(response, Http::Code::Bad_Request, "Invalid media extension!", Mime::PlainText); } @@ -534,6 +535,7 @@ void RequestHandler::MediaGetScreenshot(const Rest::Request& request, Http::Resp else if (ext == ".mkv") RequestHandlerTools::SendResource(path, response, Mime::VideoMkv); else if (ext == ".mp4") RequestHandlerTools::SendResource(path, response, Mime::VideoMp4); else if (ext == ".avi") RequestHandlerTools::SendResource(path, response, Mime::VideoAvi); + else if (ext == ".webm") RequestHandlerTools::SendResource(path, response, Mime::VideoWebm); // Unknown else RequestHandlerTools::Send(response, Http::Code::Bad_Request, "Invalid media extension!", Mime::PlainText); } @@ -542,6 +544,53 @@ void RequestHandler::MediaGetScreenshot(const Rest::Request& request, Http::Resp else RequestHandlerTools::Error404(response); } +void RequestHandler::RomsGetAll(const Rest::Request& request, Http::ResponseWriter response) +{ + RequestHandlerTools::LogRoute(request, "RomsGetAll"); +} + +void RequestHandler::RomsGetList(const Rest::Request& request, Http::ResponseWriter response) +{ + RequestHandlerTools::LogRoute(request, "RomsGetList"); + + String systemName = Url::URLDecode(request.splatAt(0).name()); + SystemData* system = mSystemManager.SystemByName(systemName); + FileData::List allGames = system->getAllGames(); + + class Serializer : public ISerializeToJson + { + public: + // Serialize a single game + JSONBuilder Serialize(const FileData* game) override + { + if (game->IsGame()) + return JSONBuilder() + .Open() + .Field("path", game->Metadata().Rom().ToString()) + .Field("name", game->Metadata().Name()) + .Field("publisher", game->Metadata().Publisher()) + .Field("developer", game->Metadata().Developer()) + .Field("genre", game->Metadata().Genre()) + .Field("players", game->Metadata().PlayerMax()) + .Field("rating", game->Metadata().Rating()) + .Close(); + return JSONBuilder(); + } + } serializer; + + JSONBuilder roms; + roms.Open() + .Field("roms", allGames, serializer) + .Close(); + + RequestHandlerTools::Send(response, Http::Code::Ok, roms, Mime::Json); +} + +void RequestHandler::RomsDelete(const Rest::Request& request, Http::ResponseWriter response) +{ + RequestHandlerTools::LogRoute(request, "RomsDelete"); +} + static const char Base64Values[] = { 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, diff --git a/projects/frontend/es-app/src/web/server/handlers/RequestHandler.h b/projects/frontend/es-app/src/web/server/handlers/RequestHandler.h index 02d30be1f41a6802b6bdbcdc4f807acf14fb87ce..46b6b0b2914146235872c815c5fcb81d663644c1 100644 --- a/projects/frontend/es-app/src/web/server/handlers/RequestHandler.h +++ b/projects/frontend/es-app/src/web/server/handlers/RequestHandler.h @@ -255,4 +255,25 @@ class RequestHandler : public IRouter * @param response Response object */ void MediaGetScreenshot(const Rest::Request& request, Http::ResponseWriter response) override; + + /*! + * @brief Handle GET to get all roms + * @param request Request object + * @param response Response object + */ + void RomsGetAll(const Rest::Request& request, Http::ResponseWriter response) override; + + /*! + * @brief Handle GET to get system roms + * @param request Request object + * @param response Response object + */ + void RomsGetList(const Rest::Request& request, Http::ResponseWriter response) override; + + /*! + * @brief Handle DELETE to delete rom + * @param request Request object + * @param response Response object + */ + void RomsDelete(const Rest::Request& request, Http::ResponseWriter response) override; }; diff --git a/projects/frontend/es-app/src/web/server/handlers/RequestHandlerTools.cpp b/projects/frontend/es-app/src/web/server/handlers/RequestHandlerTools.cpp index e90277f2daa9149a784954734be60f2dee2dfff3..aef4539fccac1576c5a0bb8f89784e97bb43eb28 100644 --- a/projects/frontend/es-app/src/web/server/handlers/RequestHandlerTools.cpp +++ b/projects/frontend/es-app/src/web/server/handlers/RequestHandlerTools.cpp @@ -26,7 +26,7 @@ void RequestHandlerTools::GetJSONMediaList(Pistache::Http::ResponseWriter& respo Path::PathList list = mediaPath.GetDirectoryContent(); static String imagesExtensions(".jpg|.jpeg|.png|.gif"); - static String videosExtensions(".mkv|.avi|.mp4"); + static String videosExtensions(".mkv|.avi|.mp4|.webm"); JSONBuilder result; result.Open() @@ -451,6 +451,7 @@ const HashMap& RequestHandlerTools::SelectConfigurationKeySet { "debuglogs" , Validator(true) }, { "pads.osd" , Validator(true) }, { "pads.osd.type" , Validator(GetAvailableOsdTypes(), false) }, + { "tateonly" , Validator(true) }, }); return sList; @@ -556,20 +557,21 @@ const HashMap& RequestHandlerTools::SelectConfigurationKeySet { static HashMap sList ({ - { "bluetooth.enabled" , Validator(true) }, - { "bluetooth.ertm" , Validator(true) }, - { "ps3.enabled" , Validator(true) }, - { "ps3.driver" , Validator(false, { "bluez", "official", "shanwan" }) }, - { "gpio.enabled" , Validator(true) }, - { "gpio.args" , Validator() }, - { "steam.enabled" , Validator(true) }, - { "db9.enabled" , Validator(true) }, - { "db9.args" , Validator() }, - { "gamecon.enabled" , Validator(true) }, - { "gamecon.args" , Validator() }, - { "xarcade.enabled" , Validator(true) }, - { "joycond.enabled" , Validator(true) }, - { "swapvalidateandcancel", Validator(true) }, + { "bluetooth.enabled" , Validator(true) }, + { "bluetooth.ertm" , Validator(true) }, + { "ps3.enabled" , Validator(true) }, + { "ps3.driver" , Validator(false, { "bluez", "official", "shanwan" }) }, + { "gpio.enabled" , Validator(true) }, + { "gpio.args" , Validator() }, + { "steam.enabled" , Validator(true) }, + { "db9.enabled" , Validator(true) }, + { "db9.args" , Validator() }, + { "gamecon.enabled" , Validator(true) }, + { "gamecon.args" , Validator() }, + { "xarcade.enabled" , Validator(true) }, + { "joycond.enabled" , Validator(true) }, + { "swapvalidateandcancel" , Validator(true) }, + { "bluetooth.autopaironboot", Validator(true) }, }); return sList; @@ -705,7 +707,7 @@ const HashMap& RequestHandlerTools::SelectConfigurationKeySet { static HashMap sList ({ - { "gamerotation", Validator(0, 3) }, // class ISerializeToJson +{ + public: + //! Virtual destructor + virtual ~ISerializeToJson() = default; + + /*! + * @brief Serialize the given object and return an appropriate JSON object + * @param object Object to serialize + * @return Object serialized to JSON + */ + virtual JSONBuilder Serialize(const T* object) = 0; +}; diff --git a/projects/frontend/es-core/src/utils/json/JSONBuilder.h b/projects/frontend/es-core/src/utils/json/JSONBuilder.h index 8797099c139663a0774aa37a6169221f6b45f828..17d9c2f2f5b174e5cba4969fdc8692e1cea3e438 100755 --- a/projects/frontend/es-core/src/utils/json/JSONBuilder.h +++ b/projects/frontend/es-core/src/utils/json/JSONBuilder.h @@ -7,6 +7,8 @@ #pragma once #include +#include +#include /*! * A very simple JSON string builder that *exactly* suits our needs. @@ -48,17 +50,16 @@ class JSONBuilder : public String */ static String Escape(const String& source) { - #define __LENGTHY_STRING(x) x, (int)sizeof(x) - 1 - String result = source; - result.Replace('\\', __LENGTHY_STRING("\\\\")) - .Replace('\b', __LENGTHY_STRING("\\b")) - .Replace('\f', __LENGTHY_STRING("\\f")) - .Replace('\n', __LENGTHY_STRING("\\n")) - .Replace('\r', __LENGTHY_STRING("\\r")) - .Replace('\t', __LENGTHY_STRING("\\t")) - .Replace('\"', __LENGTHY_STRING("\\\"")); - return result; - #undef __LENGTHY_STRING + #define LENGTHY_STRING(x) x, (int)sizeof(x) - 1 + return String(source) + .Replace('\\', LENGTHY_STRING("\\\\")) + .Replace('\b', LENGTHY_STRING("\\b")) + .Replace('\f', LENGTHY_STRING("\\f")) + .Replace('\n', LENGTHY_STRING("\\n")) + .Replace('\r', LENGTHY_STRING("\\r")) + .Replace('\t', LENGTHY_STRING("\\t")) + .Replace('\"', LENGTHY_STRING("\\\"")); + #undef LENGTHY_STRING } /*! @@ -370,4 +371,76 @@ class JSONBuilder : public String if (name != nullptr) FieldName(name).Append(':').Append(object); return *this; } + + /*! + * @brief Serialize a custom object + * @tparam T Object type + * @param name JSON Object name + * @param object Object instance + * @param serializer Custom serializer implementation + * @return The current JSON Instance for method chaining. + */ + template JSONBuilder& Field(const char* name, const T& object, ISerializeToJson& serializer) + { + if (name != nullptr) + { + OpenObject(name).Append(serializer.Serialize(&object)); + CloseObject(); + } + return *this; + } + + /*! + * @brief Serialize a custom object array + * @tparam T Object type + * @param name JSON Object name + * @param object Object array + * @param serializer Custom serializer implementation + * @return The current JSON Instance for method chaining. + */ + template JSONBuilder& Field(const char* name, const Array objects, ISerializeToJson& serializer) + { + if (name != nullptr) + { + bool notEmpty = false; + OpenArray(name); + for(const T* object : objects) + if (object != nullptr) + if (JSONBuilder jsonObject = serializer.Serialize(object); !jsonObject.empty()) + { + if (notEmpty) Append(','); + Append(jsonObject); + notEmpty = true; + } + CloseArray(); + } + return *this; + } + + /*! + * @brief Serialize a custom object array + * @tparam T Object type + * @param name JSON Object name + * @param object Object array + * @param serializer Custom serializer implementation + * @return The current JSON Instance for method chaining. + */ + template JSONBuilder& Field(const char* name, const std::vector objects, ISerializeToJson& serializer) + { + if (name != nullptr) + { + bool notEmpty = false; + OpenArray(name); + for(const T* object : objects) + if (object != nullptr) + if (JSONBuilder jsonObject = serializer.Serialize(object); !jsonObject.empty()) + { + if (notEmpty) Append(','); + Append(jsonObject); + notEmpty = true; + } + CloseArray(); + } + return *this; + } }; diff --git a/projects/frontend/external/pistache/include/pistache/mime.h b/projects/frontend/external/pistache/include/pistache/mime.h index 70cb077585134bc003bf86ad18c9a74cd93f7e01..2ca3610691a5e7a9ad33beb74af74f795a06db26 100644 --- a/projects/frontend/external/pistache/include/pistache/mime.h +++ b/projects/frontend/external/pistache/include/pistache/mime.h @@ -54,7 +54,7 @@ namespace Pistache::Http::Mime SUB_TYPE(Bmp, "bmp") \ SUB_TYPE(Jpeg, "jpeg") \ \ - /* Mime types added by recalbox */ \ + /* Mime types added by recalbox */ \ SUB_TYPE(XIcon, "x-icon") \ SUB_TYPE(Svg, "svg+xml") \ \ @@ -62,6 +62,7 @@ namespace Pistache::Http::Mime SUB_TYPE(Mp4, "mp4") \ SUB_TYPE(Avi, "x-msvideo") \ SUB_TYPE(Pdf, "pdf") \ + SUB_TYPE(Webm, "webm") \ \ SUB_TYPE(Ttf, "ttf") \ SUB_TYPE(Woff, "woff") \ diff --git a/projects/manager3/src/boot/axios.ts b/projects/manager3/src/boot/axios.ts index 2a6e41cf7ddfd2a1de0a70b1f7d22da9ea009bf9..76190e30219ba49b618c62bcd13772944a8d1e18 100644 --- a/projects/manager3/src/boot/axios.ts +++ b/projects/manager3/src/boot/axios.ts @@ -104,8 +104,10 @@ api.interceptors.response.use((response) => { let message = error; // some network errors can't be detected so... - if (!error.status) { + if (!error.response.status) { message = 'networkError'; + } else { + message = error.response.status; } Loading.hide(); diff --git a/projects/manager3/src/components/emulation/EmulationControllersTabContent.vue b/projects/manager3/src/components/emulation/EmulationControllersTabContent.vue index 9373949eef8fc80bd73e897269732790072f05de..cff3eb9cd144c20ab9b3f74c4956ea2d0de6d1b2 100755 --- a/projects/manager3/src/components/emulation/EmulationControllersTabContent.vue +++ b/projects/manager3/src/components/emulation/EmulationControllersTabContent.vue @@ -30,6 +30,18 @@ {{ $t('emulation.controllers.bluetooth.ertm.help') }} + + + diff --git a/projects/manager3/src/components/emulation/EmulationGlobalTabContent.vue b/projects/manager3/src/components/emulation/EmulationGlobalTabContent.vue index 6eda8db1a8240c583ea7a58906d6fcdba345e60e..f61e123bf149be811a5531880adb1175f174ef88 100755 --- a/projects/manager3/src/components/emulation/EmulationGlobalTabContent.vue +++ b/projects/manager3/src/components/emulation/EmulationGlobalTabContent.vue @@ -190,6 +190,29 @@ + + +
@@ -429,6 +452,7 @@ import { useEmulationstationStore } from 'stores/configuration/emulationstation' import { storeToRefs } from 'pinia'; import FormFragmentContainer from 'components/ui-kit/FormFragmentContainer.vue'; import { useAutorunStore } from 'stores/configuration/autorun'; +import { useTateStore } from 'stores/configuration/tate'; const globalStore = useGlobalStore(); globalStore.fetch(); @@ -453,4 +477,8 @@ const { const autorunStore = useAutorunStore(); autorunStore.fetch(); const { autorun } = storeToRefs(autorunStore); + +const tateStore = useTateStore(); +tateStore.fetch(); +const { tate, gameRotationOptions } = storeToRefs(tateStore); diff --git a/projects/manager3/src/components/settings/SettingsEmuStationTabContent.vue b/projects/manager3/src/components/settings/SettingsEmuStationTabContent.vue index dc810668a55886d34ade696e8cc4082758f1363e..040fbbb75e6f9458a1c60cca2084532364d9337f 100755 --- a/projects/manager3/src/components/settings/SettingsEmuStationTabContent.vue +++ b/projects/manager3/src/components/settings/SettingsEmuStationTabContent.vue @@ -19,6 +19,18 @@ {{ $t('settings.emustation.display.videomode.help') }} + + + @@ -339,6 +351,18 @@ {{ $t('settings.emustation.virtualsystems.tate.help') }} + + + diff --git a/projects/manager3/src/components/settings/SettingsSystemTabContent.vue b/projects/manager3/src/components/settings/SettingsSystemTabContent.vue index c245ec87656a32f1e3904b7aae5b35f241beec9a..c0bc787cf3493e35996144388c1fd048839d06ed 100755 --- a/projects/manager3/src/components/settings/SettingsSystemTabContent.vue +++ b/projects/manager3/src/components/settings/SettingsSystemTabContent.vue @@ -482,6 +482,7 @@ :setter="systemStore.post" apiKey="secondminitft.type" v-if="system['secondminitft.type']" + :disable="!system['secondminitft.enabled'].value" help >