diff --git a/Common/StringUtils.cpp b/Common/StringUtils.cpp index 68359c22af3a..2a8e41b7db91 100644 --- a/Common/StringUtils.cpp +++ b/Common/StringUtils.cpp @@ -356,12 +356,15 @@ void GetQuotedStrings(const std::string& str, std::vector &output) } } -std::string ReplaceAll(std::string result, const std::string& src, const std::string& dest) { +std::string ReplaceAll(std::string_view input, std::string_view src, std::string_view dest) { size_t pos = 0; + std::string result(input); + if (src == dest) return result; + // TODO: Don't mutate the input, just append stuff to the output instead. while (true) { pos = result.find(src, pos); if (pos == result.npos) diff --git a/Common/StringUtils.h b/Common/StringUtils.h index 2c3a2d7c6b86..6dd73f59091b 100644 --- a/Common/StringUtils.h +++ b/Common/StringUtils.h @@ -87,7 +87,7 @@ void SplitString(std::string_view str, const char delim, std::vector& output); -std::string ReplaceAll(std::string input, const std::string& src, const std::string& dest); +std::string ReplaceAll(std::string_view input, std::string_view src, std::string_view dest); // Takes something like R&eplace and returns Replace, plus writes 'e' to *shortcutChar // if not nullptr. Useful for Windows menu strings. diff --git a/Core/Config.cpp b/Core/Config.cpp index 845dbb2e384d..13eff5b16eca 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -266,6 +266,8 @@ static const ConfigSetting generalSettings[] = { ConfigSetting("RemoteISOSubdir", &g_Config.sRemoteISOSubdir, "/", CfgFlag::DEFAULT), ConfigSetting("RemoteDebuggerOnStartup", &g_Config.bRemoteDebuggerOnStartup, false, CfgFlag::DEFAULT), ConfigSetting("RemoteTab", &g_Config.bRemoteTab, false, CfgFlag::DEFAULT), + ConfigSetting("RemoteISOSharedDir", &g_Config.sRemoteISOSharedDir, "", CfgFlag::DEFAULT), + ConfigSetting("RemoteISOShareType", &g_Config.iRemoteISOShareType, (int)RemoteISOShareType::RECENT, CfgFlag::DEFAULT), #ifdef __ANDROID__ ConfigSetting("ScreenRotation", &g_Config.iScreenRotation, ROTATION_AUTO_HORIZONTAL), diff --git a/Core/Config.h b/Core/Config.h index fbb49859d4e5..8c7c9ef5f8b8 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -128,6 +128,8 @@ struct Config { bool bRemoteISOManual; bool bRemoteShareOnStartup; std::string sRemoteISOSubdir; + std::string sRemoteISOSharedDir; + int iRemoteISOShareType; bool bRemoteDebuggerOnStartup; bool bRemoteTab; bool bMemStickInserted; diff --git a/Core/ConfigValues.h b/Core/ConfigValues.h index 3289945532ac..4db23f461d7f 100644 --- a/Core/ConfigValues.h +++ b/Core/ConfigValues.h @@ -182,3 +182,8 @@ enum class SkipGPUReadbackMode : int { SKIP, COPY_TO_TEXTURE, }; + +enum class RemoteISOShareType : int { + RECENT, + LOCAL_FOLDER, +}; diff --git a/Core/WebServer.cpp b/Core/WebServer.cpp index 031083787228..af465fd1ae28 100644 --- a/Core/WebServer.cpp +++ b/Core/WebServer.cpp @@ -22,10 +22,12 @@ #include "Common/Net/HTTPClient.h" #include "Common/Net/HTTPServer.h" #include "Common/Net/Sinks.h" +#include "Common/Net/URL.h" #include "Common/Thread/ThreadUtil.h" #include "Common/Log.h" #include "Common/File/FileUtil.h" #include "Common/File/FileDescriptor.h" +#include "Common/File/DirListing.h" #include "Common/File/VFS/VFS.h" #include "Common/TimeUtil.h" #include "Common/StringUtils.h" @@ -49,6 +51,16 @@ static ServerStatus serverStatus; static std::mutex serverStatusLock; static int serverFlags; +// NOTE: These *only* encode spaces, which is really enough. + +std::string ServerUriEncode(std::string_view plain) { + return ReplaceAll(plain, " ", "%20"); +} + +std::string ServerUriDecode(std::string_view encoded) { + return ReplaceAll(encoded, "%20", " "); +} + static void UpdateStatus(ServerStatus s) { std::lock_guard guard(serverStatusLock); serverStatus = s; @@ -130,6 +142,12 @@ bool RemoteISOFileSupported(const std::string &filename) { } static std::string RemotePathForRecent(const std::string &filename) { + Path path(filename); + if (path.Type() == PathType::HTTP) { + // Don't re-share HTTP files from some other device. + return std::string(); + } + #ifdef _WIN32 static const std::string sep = "\\/"; #else @@ -147,19 +165,30 @@ static std::string RemotePathForRecent(const std::string &filename) { // Let's not serve directories, since they won't work. Only single files. // Maybe can do PBPs and other files later. Would be neat to stream virtual disc filesystems. if (RemoteISOFileSupported(basename)) { - return ReplaceAll(basename, " ", "%20"); + return ServerUriEncode(basename); } - return ""; + + return std::string(); } static Path LocalFromRemotePath(const std::string &path) { - for (const std::string &filename : g_Config.RecentIsos()) { - std::string basename = RemotePathForRecent(filename); - if (basename == path) { - return Path(filename); + switch ((RemoteISOShareType)g_Config.iRemoteISOShareType) { + case RemoteISOShareType::RECENT: + for (const std::string &filename : g_Config.RecentIsos()) { + std::string basename = RemotePathForRecent(filename); + if (basename == path) { + return Path(filename); + } } + return Path(); + case RemoteISOShareType::LOCAL_FOLDER: + { + std::string decoded = ServerUriDecode(path); + return Path(g_Config.sRemoteISOSharedDir) / decoded; + } + default: + return Path(); } - return Path(); } static void DiscHandler(const http::ServerRequest &request, const Path &filename) { @@ -226,12 +255,30 @@ static void HandleListing(const http::ServerRequest &request) { request.WriteHttpResponseHeader("1.0", 200, -1, "text/plain"); request.Out()->Printf("/\n"); if (serverFlags & (int)WebServerFlags::DISCS) { - // List the current discs in their recent order. - for (const std::string &filename : g_Config.RecentIsos()) { - std::string basename = RemotePathForRecent(filename); - if (!basename.empty()) { - request.Out()->Printf("%s\n", basename.c_str()); + switch ((RemoteISOShareType)g_Config.iRemoteISOShareType) { + case RemoteISOShareType::RECENT: + // List the current discs in their recent order. + for (const std::string &filename : g_Config.RecentIsos()) { + std::string basename = RemotePathForRecent(filename); + if (!basename.empty()) { + request.Out()->Printf("%s\n", basename.c_str()); + } } + break; + case RemoteISOShareType::LOCAL_FOLDER: + { + std::vector entries; + File::GetFilesInDir(Path(g_Config.sRemoteISOSharedDir), &entries); + for (const auto &entry : entries) { + // TODO: Support browsing into subdirs. How are folders marked? + if (entry.isDirectory || !RemoteISOFileSupported(entry.name)) { + continue; + } + std::string encoded = ServerUriEncode(entry.name); + request.Out()->Printf("%s\n", encoded.c_str()); + } + break; + } } } if (serverFlags & (int)WebServerFlags::DEBUGGER) { diff --git a/UI/RemoteISOScreen.cpp b/UI/RemoteISOScreen.cpp index 11f4653a7f97..003119ec1753 100644 --- a/UI/RemoteISOScreen.cpp +++ b/UI/RemoteISOScreen.cpp @@ -301,7 +301,11 @@ void RemoteISOScreen::CreateViews() { ViewGroup *rightColumn = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(300, FILL_PARENT, actionMenuMargins)); LinearLayout *rightColumnItems = new LinearLayout(ORIENT_VERTICAL); - leftColumnItems->Add(new TextView(ri->T("RemoteISODesc", "Games in your recent list will be shared"), new LinearLayoutParams(Margins(12, 5, 0, 5)))); + if ((RemoteISOShareType)g_Config.iRemoteISOShareType == RemoteISOShareType::RECENT) { + leftColumnItems->Add(new TextView(ri->T("RemoteISODesc", "Games in your recent list will be shared"), new LinearLayoutParams(Margins(12, 5, 0, 5)))); + } else { + leftColumnItems->Add(new TextView(std::string(ri->T("Share Games(Server)")) + " " + Path(g_Config.sRemoteISOSharedDir).ToVisualString(), new LinearLayoutParams(Margins(12, 5, 0, 5)))); + } leftColumnItems->Add(new TextView(ri->T("RemoteISOWifi", "Note: Connect both devices to the same wifi"), new LinearLayoutParams(Margins(12, 5, 0, 5)))); firewallWarning_ = leftColumnItems->Add(new TextView(ri->T("RemoteISOWinFirewall", "WARNING: Windows Firewall is blocking sharing"), new LinearLayoutParams(Margins(12, 5, 0, 5)))); firewallWarning_->SetTextColor(0xFF0000FF); @@ -589,8 +593,19 @@ void RemoteISOSettingsScreen::CreateViews() { remoteisoSettings->Add(new CheckBox(&g_Config.bRemoteISOManual, ri->T("Manual Mode Client", "Manually configure client"))); remoteisoSettings->Add(new CheckBox(&g_Config.bRemoteTab, ri->T("Show Remote tab on main screen"))); - UI::Choice *remoteServer; - remoteServer = new PopupTextInputChoice(&g_Config.sLastRemoteISOServer, ri->T("Remote Server"), "", 255, screenManager()); + if (System_GetPropertyBool(SYSPROP_HAS_FOLDER_BROWSER)) { + static const char *shareTypes[] = { "Recent files", "Choose directory" }; + remoteisoSettings->Add(new PopupMultiChoice(&g_Config.iRemoteISOShareType, ri->T("Files to share"), shareTypes, 0, ARRAY_SIZE(shareTypes), I18NCat::REMOTEISO, screenManager())); + FolderChooserChoice *folderChooser = remoteisoSettings->Add(new FolderChooserChoice(&g_Config.sRemoteISOSharedDir, ri->T("Files to share"))); + folderChooser->SetEnabledFunc([=]() { + return g_Config.iRemoteISOShareType == (int)RemoteISOShareType::LOCAL_FOLDER; + }); + } else { + // Can't pick a folder, only allow sharing recent stuff. + g_Config.iRemoteISOShareType = (int)RemoteISOShareType::RECENT; + } + + UI::Choice *remoteServer = new PopupTextInputChoice(&g_Config.sLastRemoteISOServer, ri->T("Remote Server"), "", 255, screenManager()); remoteisoSettings->Add(remoteServer); remoteServer->SetEnabledPtr(&g_Config.bRemoteISOManual);