mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-31 19:47:06 +01:00
The static string was only initialized on the first call, so the text editor (used where there's no native input dialog) showed whatever was typed the first time, even after picking another server from the list. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
881 lines
29 KiB
C++
881 lines
29 KiB
C++
#include <algorithm>
|
|
#include <limits>
|
|
|
|
#include "ppsspp_config.h"
|
|
|
|
#undef new
|
|
#ifdef SYSTEM_RAPIDJSON
|
|
#include <rapidjson/document.h>
|
|
#else
|
|
#include "ext/rapidjson/include/rapidjson/document.h"
|
|
#endif
|
|
#include "ext/pugixml/pugixml.hpp"
|
|
#include "Common/DbgNew.h"
|
|
|
|
#include "AdhocServerScreen.h"
|
|
#include "Core/Util/GameDB.h"
|
|
|
|
#include "Common/Net/Resolve.h"
|
|
#include "Common/UI/Root.h"
|
|
#include "Common/UI/PopupScreens.h"
|
|
#include "Common/UI/ScreenManager.h"
|
|
#include "Common/Data/Text/Parsers.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Common/Net/HTTPClient.h"
|
|
#include "Core/HLE/sceNetAdhoc.h"
|
|
#include "UI/MiscViews.h"
|
|
|
|
static void UpgradeGameName(std::string *str) {
|
|
if (str->size() == 9) { // TODO: Make a better heuristic, we might make some failed lookup into the DB.
|
|
// It's probably a game ID. Convert it to a name using the database.
|
|
std::vector<GameDBInfo> infos;
|
|
if (g_gameDB.GetGameInfos(*str, &infos)) {
|
|
*str = infos[0].title;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int ParseUserCountValue(const rapidjson::Value &v) {
|
|
if (v.IsInt())
|
|
return v.GetInt();
|
|
else if (v.IsString()) {
|
|
int value = 0;
|
|
if (TryParse(v.GetString(), &value))
|
|
return value;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int ParsePortValue(const rapidjson::Value &v) {
|
|
if (v.IsInt())
|
|
return v.GetInt();
|
|
return -1;
|
|
}
|
|
|
|
std::vector<AdhocGame> ParseStatusXML(const std::string& xmlInput) {
|
|
pugi::xml_document doc;
|
|
pugi::xml_parse_result result = doc.load_string(xmlInput.c_str());
|
|
|
|
std::vector<AdhocGame> gameList;
|
|
if (!result) {
|
|
ERROR_LOG(Log::sceNet, "XML Parsing Error: %s", result.description());
|
|
return gameList;
|
|
}
|
|
|
|
// Root is <prometheus>
|
|
pugi::xml_node prometheus = doc.child("prometheus");
|
|
|
|
for (pugi::xml_node xmlGame : prometheus.children("game")) {
|
|
AdhocGame game;
|
|
game.name = xmlGame.attribute("name").as_string();
|
|
game.usercount = xmlGame.attribute("usercount").as_int();
|
|
|
|
for (pugi::xml_node xmlGroup : xmlGame.children("group")) {
|
|
AdhocGroup group;
|
|
group.name = xmlGroup.attribute("name").as_string();
|
|
group.usercount = xmlGroup.attribute("usercount").as_int();
|
|
|
|
for (pugi::xml_node xmlUser : xmlGroup.children("user")) {
|
|
AdhocUser user;
|
|
// In XML, the username is the text inside the <user> tag
|
|
user.name = xmlUser.child_value();
|
|
group.users.push_back(user);
|
|
}
|
|
game.groups.push_back(group);
|
|
}
|
|
gameList.push_back(game);
|
|
}
|
|
|
|
return gameList;
|
|
}
|
|
|
|
std::vector<AdhocGame> ParseDataJson(std::string_view json) {
|
|
rapidjson::Document d;
|
|
d.Parse(json.data(), json.size());
|
|
|
|
std::vector<AdhocGame> gameList;
|
|
|
|
if (d.HasParseError() || !d.IsObject() || !d.HasMember("games") || !d["games"].IsArray())
|
|
return gameList;
|
|
|
|
const auto& gamesArray = d["games"];
|
|
for (auto& g : gamesArray.GetArray()) {
|
|
if (!g.IsObject())
|
|
continue;
|
|
|
|
AdhocGame game;
|
|
if (!g.HasMember("name") || !g["name"].IsString())
|
|
continue;
|
|
game.name = g["name"].GetString();
|
|
UpgradeGameName(&game.name);
|
|
|
|
game.usercount = g.HasMember("usercount") ? ParseUserCountValue(g["usercount"]) : 0;
|
|
|
|
if (g.HasMember("game_ids") && g["game_ids"].IsArray()) {
|
|
for (auto& id : g["game_ids"].GetArray()) {
|
|
if (!id.IsString())
|
|
continue;
|
|
game.game_ids.push_back(id.GetString());
|
|
}
|
|
}
|
|
|
|
if (g.HasMember("groups") && g["groups"].IsArray()) {
|
|
for (auto& grp : g["groups"].GetArray()) {
|
|
if (!grp.IsObject())
|
|
continue;
|
|
|
|
AdhocGroup group;
|
|
if (!grp.HasMember("name") || !grp["name"].IsString())
|
|
continue;
|
|
group.name = grp["name"].GetString();
|
|
group.usercount = grp.HasMember("usercount") ? ParseUserCountValue(grp["usercount"]) : 0;
|
|
|
|
if (grp.HasMember("users") && grp["users"].IsArray()) {
|
|
for (auto& u : grp["users"].GetArray()) {
|
|
if (!u.IsObject() || !u.HasMember("name") || !u["name"].IsString())
|
|
continue;
|
|
|
|
AdhocUser user;
|
|
user.name = u["name"].GetString();
|
|
|
|
if (u.HasMember("pdp_ports") && u["pdp_ports"].IsArray()) {
|
|
for (auto& p : u["pdp_ports"].GetArray()) {
|
|
int port = ParsePortValue(p);
|
|
if (port >= 0)
|
|
user.pdp_ports.push_back(port);
|
|
}
|
|
}
|
|
|
|
if (u.HasMember("ptp_ports") && u["ptp_ports"].IsArray()) {
|
|
for (auto& p : u["ptp_ports"].GetArray()) {
|
|
int port = ParsePortValue(p);
|
|
if (port >= 0)
|
|
user.ptp_ports.push_back(port);
|
|
}
|
|
}
|
|
|
|
group.users.push_back(user);
|
|
}
|
|
}
|
|
game.groups.push_back(group);
|
|
}
|
|
}
|
|
gameList.push_back(game);
|
|
}
|
|
return gameList;
|
|
}
|
|
|
|
class AdhocAddServerPopupScreen : public UI::PopupScreen {
|
|
public:
|
|
AdhocAddServerPopupScreen(std::string *outEditValue) : PopupScreen(T(I18NCat::NETWORKING, "Add server"), T(I18NCat::DIALOG, "Add"), T(I18NCat::DIALOG, "Cancel")), outEditValue_(outEditValue) {
|
|
}
|
|
|
|
void CreatePopupContents(UI::ViewGroup *parent) override {
|
|
using namespace UI;
|
|
auto ni = GetI18NCategory(I18NCat::NETWORKING);
|
|
|
|
PopupTextInputChoice *textInputChoice = parent->Add(new PopupTextInputChoice(GetRequesterToken(), &editValue_, ni->T("Hostname or IP"), "", 450, screenManager()));
|
|
textInputChoice->SetShadowText(ni->T("Hostname or IP"));
|
|
parent->Add(new CheckBox(&hasRelay_, ni->T("Relay server mode")));
|
|
}
|
|
|
|
virtual void OnCompleted(DialogResult result) override {
|
|
if (result == DialogResult::DR_OK) {
|
|
std::vector<AdhocServerListEntry> servers = AdhocGetServerList(AdhocLoadListMode::CacheOnlySync);
|
|
bool preset = false;
|
|
for (auto &iter : servers) {
|
|
if (equalsNoCase(editValue_, iter.host)) {
|
|
// We have this predefined.
|
|
preset = true;
|
|
}
|
|
}
|
|
if (!preset) {
|
|
if (hasRelay_) {
|
|
// Insert at the start of the vector.
|
|
if (!ContainsNoCase(g_Config.vCustomAdhocServerListWithRelay, editValue_) &&
|
|
!ContainsNoCase(g_Config.vCustomAdhocServerList, editValue_)) {
|
|
g_Config.vCustomAdhocServerListWithRelay.insert(g_Config.vCustomAdhocServerListWithRelay.begin(), editValue_);
|
|
}
|
|
} else {
|
|
if (!ContainsNoCase(g_Config.vCustomAdhocServerList, editValue_) &&
|
|
!ContainsNoCase(g_Config.vCustomAdhocServerListWithRelay, editValue_)) {
|
|
g_Config.vCustomAdhocServerList.insert(g_Config.vCustomAdhocServerList.begin(), editValue_);
|
|
}
|
|
}
|
|
}
|
|
*outEditValue_ = editValue_;
|
|
}
|
|
}
|
|
virtual bool CanComplete(DialogResult result) override { return result == DR_OK ? !editValue_.empty() : true; }
|
|
|
|
const char *tag() const override { return "AdhocAddServerPopup"; }
|
|
|
|
private:
|
|
std::string editValue_;
|
|
std::string *outEditValue_;
|
|
bool hasRelay_ = false;
|
|
};
|
|
|
|
AdhocServerCompactInfo::AdhocServerCompactInfo(const AdhocServerListEntry &entry, UI::LayoutParams *layoutParams)
|
|
: UI::LinearLayout(ORIENT_HORIZONTAL, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT, UI::Margins(5.0f, 0.0f))), entry_(entry) {
|
|
using namespace UI;
|
|
|
|
SetSpacing(5.0f);
|
|
|
|
LinearLayout *lines = Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(Margins(5, 5))));
|
|
lines->SetSpacing(0.0f);
|
|
TextView *name = lines->Add(new TextView(entry.name));
|
|
|
|
std::string secondLine = entry.host;
|
|
auto n = GetI18NCategory(I18NCat::NETWORKING);
|
|
if (!entry.location.empty()) {
|
|
secondLine += ": " + entry.location;
|
|
}
|
|
|
|
lines->Add(new TextView(secondLine))->SetTextSize(TextSize::Small)->SetWordWrap();
|
|
|
|
Add(new Spacer(0.0f, new LinearLayoutParams(1.0f, Margins(0.0f, 5.0f))));
|
|
|
|
if (entry.mode == AdhocDataMode::AemuPostoffice) {
|
|
TextView *relay = Add(new TextView(n->T("Relay"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity::G_VCENTER, Margins(10.0))));
|
|
}
|
|
|
|
Add(new Choice(ImageID("I_FILE_COPY"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT)))->OnClick.Add([host = entry_.host](UI::EventParams &) {
|
|
System_CopyStringToClipboard(host);
|
|
});
|
|
}
|
|
|
|
void AdhocServerCompactInfo::Draw(UIContext &dc) {
|
|
UI::LinearLayout::Draw(dc);
|
|
// Underline
|
|
dc.Draw()->DrawImageCenterTexel(dc.GetTheme().whiteImage, bounds_.x, bounds_.y2() - 2, bounds_.x2(), bounds_.y2(), dc.GetTheme().popupTitleStyle.fgColor);
|
|
}
|
|
|
|
static UI::View *CreateInfoItemWithButton(std::string_view text, ImageID buttonImage, std::function<void(UI::EventParams &)> onClick) {
|
|
using namespace UI;
|
|
LinearLayout *line = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(12, 0)));
|
|
line->Add(new TextView(text, new LinearLayoutParams(0.0f, Gravity::G_VCENTER)));
|
|
line->Add(new Spacer(0, new LinearLayoutParams(1.0f)));
|
|
line->Add(new Choice(buttonImage, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT)))->OnClick.Add(onClick);
|
|
return line;
|
|
}
|
|
|
|
static UI::View *CreateLinkButton(std::string url, std::string_view title = "") {
|
|
using namespace UI;
|
|
|
|
// steal strings from all over the place
|
|
auto cr = GetI18NCategory(I18NCat::PSPCREDITS);
|
|
auto st = GetI18NCategory(I18NCat::STORE);
|
|
|
|
ImageID icon = ImageID("I_LINK_OUT_QUESTION");
|
|
if (startsWith(url, "https://discord")) {
|
|
icon = ImageID("I_LOGO_DISCORD");
|
|
if (title.empty())
|
|
title = cr->T("Discord");
|
|
} else {
|
|
icon = ImageID("I_LINK_OUT");
|
|
if (title.empty())
|
|
title = st->T("Website");
|
|
}
|
|
|
|
Choice *choice = new Choice(title, icon, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT));
|
|
choice->OnClick.Add([url](UI::EventParams &) {
|
|
System_LaunchUrl(LaunchUrlType::BROWSER_URL, url);
|
|
});
|
|
return choice;
|
|
}
|
|
|
|
// Later, this might also show games-in-progress.
|
|
// For now, it's just a simple metadata viewer.
|
|
AdhocServerInfoScreen::AdhocServerInfoScreen(const AdhocServerListEntry &entry)
|
|
: UI::PopupScreen("", T(I18NCat::DIALOG, "Back")), entry_(entry) {
|
|
|
|
std::string dataUrl;
|
|
if (!entry.dataJsonUrl.empty()) {
|
|
dataUrl = http::RemoveHttpsIfNeeded(entry.dataJsonUrl);
|
|
} else if (!entry.statusXmlUrl.empty()) {
|
|
dataUrl = http::RemoveHttpsIfNeeded(entry.statusXmlUrl);
|
|
}
|
|
|
|
if (!dataUrl.empty()) {
|
|
statusRequest_ = g_DownloadManager.StartDownload(dataUrl, Path(), http::RequestFlags::KeepInMemory, nullptr, "status");
|
|
}
|
|
}
|
|
|
|
void CreateAdhocServerGameList(UI::ViewGroup *content, const std::vector<AdhocGame> &games, bool requestInProgress) {
|
|
using namespace UI;
|
|
auto ni = GetI18NCategory(I18NCat::NETWORKING);
|
|
if (games.empty()) {
|
|
if (requestInProgress) {
|
|
// Still loading. Show a spinner.
|
|
content->Add(new Spinner(nullptr, 0, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Margins(8))));
|
|
} else {
|
|
content->Add(new TextView(ni->T("No games in progress on this server")));
|
|
}
|
|
return;
|
|
}
|
|
for (const AdhocGame &game : games) {
|
|
std::string title = game.name + " - " + ApplySafeSubstitutions(ni->T("players: %1"), game.usercount) + " " + ApplySafeSubstitutions(ni->T("groups: %1"), (int)game.groups.size());
|
|
CollapsibleSection *gameSection = content->Add(new CollapsibleSection(title));
|
|
gameSection->Header()->SetUnderline(false);
|
|
for (const AdhocGroup &group : game.groups) {
|
|
std::string groupName(StripSpaces(group.name));
|
|
if (groupName.empty()) {
|
|
groupName = "???";
|
|
}
|
|
if (group.usercount >= 1 && groupName == "Groupless") {
|
|
gameSection->Add(new TextView(" " + ApplySafeSubstitutions(ni->T("Players waiting: %1"), group.usercount)))->SetTextSize(TextSize::Small);
|
|
continue;
|
|
}
|
|
if (g_Config.bAdhocServerShowPlayerPorts) {
|
|
// Show detailed info, each player on their own line.
|
|
gameSection->Add(new TextView(" " + groupName + " - " + ApplySafeSubstitutions(ni->T("players: %1"), group.usercount)))->SetTextSize(TextSize::Small);
|
|
for (const AdhocUser &user : group.users) {
|
|
std::string portInfo;
|
|
if (!user.pdp_ports.empty()) {
|
|
portInfo += "PDP: ";
|
|
for (int port : user.pdp_ports) {
|
|
portInfo += std::to_string(port) + " ";
|
|
}
|
|
}
|
|
if (!user.ptp_ports.empty()) {
|
|
portInfo += "PTP: ";
|
|
for (int port : user.ptp_ports) {
|
|
portInfo += std::to_string(port) + " ";
|
|
}
|
|
}
|
|
gameSection->Add(new TextView(" " + user.name + " " + portInfo))->SetTextSize(TextSize::Tiny);
|
|
}
|
|
} else {
|
|
// Show each group on a single line.
|
|
std::string groupString = " " + groupName + ApplySafeSubstitutions(" (%1):", group.usercount);
|
|
bool first = true;
|
|
for (const AdhocUser &user : group.users) {
|
|
if (first) {
|
|
groupString += " ";
|
|
first = false;
|
|
} else {
|
|
groupString += ", ";
|
|
}
|
|
groupString += user.name;
|
|
}
|
|
gameSection->Add(new TextView(groupString))->SetTextSize(TextSize::Small)->SetWordWrap();
|
|
}
|
|
}
|
|
gameSection->SetOpen(false); // NOTE: Must be last!
|
|
}
|
|
}
|
|
|
|
void AdhocServerInfoScreen::CreatePopupContents(UI::ViewGroup *parent) {
|
|
using namespace UI;
|
|
auto pa = GetI18NCategory(I18NCat::PAUSE);
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
auto ni = GetI18NCategory(I18NCat::NETWORKING);
|
|
|
|
Margins contentMargins(12, 0);
|
|
|
|
parent->Add(new AdhocServerCompactInfo(entry_, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, contentMargins)));
|
|
parent->Add(new Spacer(5.0f));
|
|
|
|
ScrollView *scroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f));
|
|
LinearLayout *content = new LinearLayout(ORIENT_VERTICAL);
|
|
content->SetSpacing(6.0f);
|
|
if (!entry_.ip.empty()) {
|
|
content->Add(new InfoItem(entry_.ip, ""));
|
|
}
|
|
TextView *desc = content->Add(new TextView(entry_.description, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, contentMargins)));
|
|
desc->SetTextSize(TextSize::Small);
|
|
desc->SetWordWrap();
|
|
|
|
LinearLayout *buttonStrip = content->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, contentMargins)));
|
|
buttonStrip->SetSpacing(8);
|
|
if (!entry_.web.empty() || !entry_.discord.empty()) {
|
|
if (!entry_.web.empty()) {
|
|
buttonStrip->Add(CreateLinkButton(entry_.web));
|
|
}
|
|
if (!entry_.discord.empty()) {
|
|
buttonStrip->Add(CreateLinkButton(entry_.discord));
|
|
}
|
|
}
|
|
|
|
if (entry_.dataJsonUrl.empty() && entry_.statusXmlUrl.empty()) {
|
|
content->Add(CreateInfoItemWithButton(ni->T("This server has no data.json status page"), ImageID("I_LINK_OUT_QUESTION"), [](UI::EventParams &e) {
|
|
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org/docs/multiplayer/adhoc-server-status/");
|
|
}));
|
|
if (!entry_.statusWebUrl.empty()) {
|
|
buttonStrip->Add(CreateLinkButton(entry_.statusWebUrl, ni->T("Status")));
|
|
}
|
|
if (!entry_.statusXmlUrl.empty()) {
|
|
buttonStrip->Add(CreateLinkButton(entry_.statusXmlUrl, ni->T("Status")));
|
|
}
|
|
} else {
|
|
CreateAdhocServerGameList(content, games_, statusRequest_.get() ? true : false);
|
|
}
|
|
|
|
scroll->Add(content);
|
|
parent->Add(scroll);
|
|
}
|
|
|
|
void AdhocServerInfoScreen::update() {
|
|
UI::PopupScreen::update();
|
|
if (statusRequest_ && statusRequest_->Done()) {
|
|
std::string data;
|
|
statusRequest_->buffer().TakeAll(&data);
|
|
if (endsWith(statusRequest_->url(), ".xml")) {
|
|
games_ = ParseStatusXML(data);
|
|
} else {
|
|
games_ = ParseDataJson(data);
|
|
}
|
|
statusRequest_.reset();
|
|
RecreateViews();
|
|
}
|
|
}
|
|
|
|
void AddDeleteButton(std::string *editValue, ScreenManager *screenManager, UI::ViewGroup *viewGroup, const AdhocServerListEntry &entry) {
|
|
using namespace UI;
|
|
Choice *deleteButton = viewGroup->Add(new Choice(ImageID("I_TRASHCAN"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity::G_VCENTER, Margins(0, 0, 10, 0))));
|
|
deleteButton->OnClick.Add([host = entry.host, screenManager, editValue](UI::EventParams &e) {
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
const std::string quotedHost = "\"" + host + "\"";
|
|
const std::string message = ApplySafeSubstitutions(di->T("Are you sure you want to delete %1?"), quotedHost);
|
|
screenManager->push(new UI::MessagePopupScreen(di->T("Delete"), message, di->T("Delete"), di->T("Cancel"), [host, editValue](bool confirmed) {
|
|
if (confirmed) {
|
|
RemoveNoCase(g_Config.vCustomAdhocServerList, host);
|
|
RemoveNoCase(g_Config.vCustomAdhocServerListWithRelay, host);
|
|
if (*editValue == host) {
|
|
// Reset to socom.cc, which will always be in a list.
|
|
*editValue = DefaultProAdhocServer();
|
|
}
|
|
}
|
|
}));
|
|
});
|
|
}
|
|
|
|
class AdhocServerRow : public UI::LinearLayout {
|
|
public:
|
|
AdhocServerRow(std::string *value, const AdhocServerListEntry &entry, bool showDeleteButton, ScreenManager *screenManager, UI::LayoutParams *layoutParams = nullptr);
|
|
|
|
void GetContentDimensions(const UIContext &dc, float &w, float &h) const override {
|
|
w = 500; h = 90;
|
|
}
|
|
|
|
void Draw(UIContext &dc) override {
|
|
dc.FillRect(dc.GetTheme().itemStyle.background, bounds_);
|
|
if (*value_ == entry_.host) {
|
|
// TODO: Make this highlight themable
|
|
dc.FillRect(UI::Drawable(0x48FFFFFF), GetBounds());
|
|
}
|
|
LinearLayout::Draw(dc);
|
|
}
|
|
|
|
bool Touch(const TouchInput &input) override {
|
|
using namespace UI;
|
|
if (UI::LinearLayout::Touch(input)) {
|
|
return true;
|
|
}
|
|
if (input.flags & TouchInputFlags::DOWN) {
|
|
if (bounds_.Contains(input.x, input.y)) {
|
|
dragging_ = true;
|
|
return true;
|
|
}
|
|
}
|
|
if (dragging_ && (input.flags & TouchInputFlags::UP)) {
|
|
dragging_ = false;
|
|
if (!(input.flags & TouchInputFlags::CANCEL) && bounds_.Contains(input.x, input.y)) {
|
|
EventParams e;
|
|
e.v = this;
|
|
OnSelected.Trigger(e);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
UI::Event OnSelected;
|
|
|
|
private:
|
|
bool dragging_ = false;
|
|
std::string *value_;
|
|
AdhocServerListEntry entry_;
|
|
};
|
|
|
|
AdhocServerRow::AdhocServerRow(std::string *editValue, const AdhocServerListEntry &entry, bool showDeleteButton, ScreenManager *screenManager, UI::LayoutParams *layoutParams)
|
|
: UI::LinearLayout(ORIENT_HORIZONTAL, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT, UI::Margins(5.0f, 0.0f))), value_(editValue), entry_(entry) {
|
|
using namespace UI;
|
|
|
|
SetSpacing(5.0f);
|
|
// Show as radio button to make it really clear that selection actually is the choice.
|
|
int number = 0;
|
|
Add(new ImageView([editValue, host = entry.host]() { return host == *editValue ? ImageID("I_RADIO_SELECTED") : ImageID("I_RADIO_EMPTY"); },
|
|
new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity::G_VCENTER, Margins(5, 0, 0, 0))));
|
|
|
|
LinearLayout *lines = Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(Margins(5, 5))));
|
|
lines->SetSpacing(0.0f);
|
|
ClickableTextView *name = lines->Add(new ClickableTextView(entry.name));
|
|
name->SetFocusable(true);
|
|
name->OnClick.Add([this](UI::EventParams &e) {
|
|
EventParams e2;
|
|
e2.v = this;
|
|
OnSelected.Trigger(e2);
|
|
});
|
|
|
|
std::string secondLine = entry.host;
|
|
auto n = GetI18NCategory(I18NCat::NETWORKING);
|
|
if (entry.host == "localhost") {
|
|
// Special case this to add a hint.
|
|
secondLine = n->T("Ad hoc server address hint");
|
|
}
|
|
if (!entry.location.empty()) {
|
|
secondLine += ": " + entry.location;
|
|
}
|
|
|
|
lines->Add(new TextView(secondLine))->SetTextSize(TextSize::Small)->SetWordWrap();
|
|
|
|
Add(new Spacer(0.0f, new LinearLayoutParams(1.0f, Margins(0.0f, 5.0f))));
|
|
|
|
if (entry.mode == AdhocDataMode::AemuPostoffice) {
|
|
TextView *relay = Add(new TextView(n->T("Relay"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity::G_VCENTER, Margins(10.0))));
|
|
}
|
|
if (showDeleteButton) {
|
|
AddDeleteButton(editValue, screenManager, this, entry);
|
|
}
|
|
|
|
if (!entry.description.empty()) {
|
|
Choice *infoButton = Add(new Choice(ImageID("I_INFO"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity::G_VCENTER, Margins(0, 0, 10, 0))));
|
|
AdhocServerListEntry copy = entry;
|
|
infoButton->OnClick.Add([this, copy, screenManager](UI::EventParams &e) {
|
|
e.v = this;
|
|
screenManager->push(new AdhocServerInfoScreen(copy));
|
|
});
|
|
}
|
|
}
|
|
|
|
AdhocServerScreen::AdhocServerScreen(std::string *value, std::string_view title)
|
|
: UI::PopupScreen(title, T(I18NCat::DIALOG, "OK"), T(I18NCat::DIALOG, "Cancel")), value_(value) {
|
|
resolver_ = std::thread([](AdhocServerScreen *thiz) {
|
|
thiz->ResolverThread();
|
|
}, this);
|
|
editValue_ = *value;
|
|
AdhocLoadServerList(AdhocLoadListMode::AllSourcesAsync);
|
|
}
|
|
|
|
AdhocServerScreen::~AdhocServerScreen() {
|
|
{
|
|
std::unique_lock<std::mutex> guard(resolverLock_);
|
|
resolverState_ = ResolverState::QUIT;
|
|
resolverCond_.notify_one();
|
|
}
|
|
resolver_.join();
|
|
}
|
|
|
|
void AdhocServerScreen::sendMessage(UIMessage message, const char *value) {
|
|
if (message == UIMessage::ADHOC_SERVER_LIST_CHANGED) {
|
|
RecreateViews();
|
|
}
|
|
}
|
|
|
|
void AdhocServerScreen::CreatePopupContents(UI::ViewGroup *parent) {
|
|
using namespace UI;
|
|
auto sy = GetI18NCategory(I18NCat::SYSTEM);
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
auto n = GetI18NCategory(I18NCat::NETWORKING);
|
|
|
|
// We already kicked off loading the server list in the constructor, so by now it should be either loaded or in the process of loading.
|
|
// We can get the cached list immediately, and then update it when the async load finishes.
|
|
std::vector<AdhocServerListEntry> listItems = AdhocGetServerList(AdhocLoadListMode::CacheOnlySync);
|
|
|
|
Choice *addServer = parent->Add(new Choice(n->T("Add server"), ImageID("I_PLUS")));
|
|
addServer->OnClick.Add([this](UI::EventParams &e) {
|
|
AdhocAddServerPopupScreen *addScreen = new AdhocAddServerPopupScreen(&editValue_);
|
|
screenManager()->push(addScreen);
|
|
});
|
|
|
|
parent->Add(new Spacer(5.0f));
|
|
|
|
// editValue_ has the currently selected server. On closing the dialog, we copy that to settings.
|
|
|
|
// Start with the downloaded list.
|
|
std::vector<AdhocServerListEntry> entries = listItems;
|
|
std::vector<AdhocServerListEntry> localEntries;
|
|
std::vector<AdhocServerListEntry> customEntries;
|
|
|
|
bool currentServerFound = false; // If the current server is not found, we'll have to add it to one of the lists.
|
|
|
|
for (const auto &iter : entries) {
|
|
if (iter.host == editValue_) {
|
|
currentServerFound = true;
|
|
}
|
|
}
|
|
|
|
// Add localhost and local IPs, since those are common ones to connect to.
|
|
{
|
|
AdhocServerListEntry localhostEntry;
|
|
localhostEntry.name = "localhost";
|
|
localhostEntry.host = "localhost";
|
|
localEntries.push_back(localhostEntry);
|
|
|
|
std::vector<std::string> listIP;
|
|
net::GetLocalIP4List(listIP);
|
|
|
|
for (const auto &ipAddress : listIP) {
|
|
if (startsWith(ipAddress, "127.") || startsWith(ipAddress, "169.254.") || startsWith(ipAddress, "0.")) {
|
|
continue;
|
|
}
|
|
AdhocServerListEntry entry;
|
|
entry.name = ipAddress;
|
|
entry.host = ipAddress;
|
|
localEntries.push_back(entry);
|
|
|
|
if (ipAddress == editValue_) {
|
|
currentServerFound = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
auto hostInEntries = [entries](const std::string &host) {
|
|
for (const auto &entry : entries) {
|
|
if (entry.host == host) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
for (auto iter = g_Config.vCustomAdhocServerListWithRelay.begin(); iter != g_Config.vCustomAdhocServerListWithRelay.end();) {
|
|
// If the host is already in the public list, skip it. We don't want duplicates.
|
|
if (hostInEntries(*iter) || iter->empty()) {
|
|
// Remove things that duplicate the public list, or that are empty (probably added by mistake).
|
|
iter = g_Config.vCustomAdhocServerListWithRelay.erase(iter);
|
|
recreateParent_ = true;
|
|
continue;
|
|
}
|
|
AdhocServerListEntry entry;
|
|
entry.name = *iter;
|
|
entry.host = *iter;
|
|
entry.mode = AdhocDataMode::AemuPostoffice;
|
|
customEntries.push_back(entry);
|
|
|
|
if (*iter == editValue_) {
|
|
currentServerFound = true;
|
|
}
|
|
iter++;
|
|
}
|
|
|
|
for (auto iter = g_Config.vCustomAdhocServerList.begin(); iter != g_Config.vCustomAdhocServerList.end();) {
|
|
// If the host is already in the public list, skip it. We don't want duplicates.
|
|
if (hostInEntries(*iter) || iter->empty()) {
|
|
// Remove things that duplicate the public list, or that are empty (probably added by mistake).
|
|
iter = g_Config.vCustomAdhocServerList.erase(iter);
|
|
recreateParent_ = true;
|
|
continue;
|
|
}
|
|
AdhocServerListEntry entry;
|
|
entry.name = *iter;
|
|
entry.host = *iter;
|
|
entry.mode = AdhocDataMode::P2P;
|
|
customEntries.push_back(entry);
|
|
|
|
if (*iter == editValue_) {
|
|
currentServerFound = true;
|
|
}
|
|
iter++;
|
|
}
|
|
|
|
ScrollView *scrollView = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0f));
|
|
LinearLayout *innerView = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
|
|
innerView->SetSpacing(5.0f);
|
|
|
|
auto AddButtonFromEntry = [this](UI::ViewGroup *parent, const AdhocServerListEntry &entry, bool showDeleteButton) {
|
|
AdhocServerRow *row = new AdhocServerRow(&editValue_, entry, showDeleteButton, screenManager());
|
|
parent->Add(row);
|
|
row->OnSelected.Add([this](UI::EventParams &e) {
|
|
std::string value = e.v->Tag();
|
|
if (!value.empty()) {
|
|
editValue_ = value;
|
|
}
|
|
});
|
|
row->SetTag(entry.host);
|
|
};
|
|
|
|
if (!customEntries.empty()) {
|
|
CollapsibleSection *customSection = innerView->Add(new CollapsibleSection(n->T("Custom server list"), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
|
|
|
|
if (!currentServerFound) {
|
|
// Add the entry to one of the lists.
|
|
AdhocServerListEntry entry;
|
|
entry.name = editValue_;
|
|
entry.host = editValue_;
|
|
// Let's do a heuristic, we don't have a good value here..
|
|
entry.mode = (AdhocServerRelayMode)g_Config.iAdhocServerRelayMode == AdhocServerRelayMode::AlwaysOn ? AdhocDataMode::AemuPostoffice : AdhocDataMode::P2P;
|
|
if (entry.mode == AdhocDataMode::AemuPostoffice) {
|
|
g_Config.vCustomAdhocServerListWithRelay.insert(g_Config.vCustomAdhocServerListWithRelay.begin(), editValue_);
|
|
} else {
|
|
g_Config.vCustomAdhocServerList.insert(g_Config.vCustomAdhocServerList.begin(), editValue_);
|
|
}
|
|
recreateParent_ = true;
|
|
AddButtonFromEntry(customSection, entry, true);
|
|
}
|
|
|
|
for (const auto &entry : customEntries) {
|
|
AddButtonFromEntry(customSection, entry, true);
|
|
}
|
|
}
|
|
|
|
CollapsibleSection *publicSection = innerView->Add(new CollapsibleSection(n->T("Public server list"), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
|
|
for (const auto &entry : entries) {
|
|
// Show even hidden entries, as long as they are chosen currently.
|
|
if (entry.hidden && entry.host != g_Config.sProAdhocServer)
|
|
continue;
|
|
AddButtonFromEntry(publicSection, entry, false);
|
|
}
|
|
|
|
CollapsibleSection *localSection = innerView->Add(new CollapsibleSection(n->T("Local network addresses"), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
|
|
for (const auto &entry : localEntries) {
|
|
AddButtonFromEntry(localSection, entry, false);
|
|
}
|
|
|
|
scrollView->Add(innerView);
|
|
parent->Add(scrollView);
|
|
|
|
progressView_ = parent->Add(new NoticeView(NoticeLevel::INFO, n->T("Validating address..."), "", new LinearLayoutParams(Margins(0, 5, 0, 0))));
|
|
progressView_->SetVisibility(UI::V_GONE);
|
|
}
|
|
|
|
void AdhocServerScreen::ResolverThread() {
|
|
std::unique_lock<std::mutex> guard(resolverLock_);
|
|
|
|
while (resolverState_ != ResolverState::QUIT) {
|
|
resolverCond_.wait(guard);
|
|
|
|
if (resolverState_ == ResolverState::QUEUED) {
|
|
resolverState_ = ResolverState::PROGRESS;
|
|
|
|
addrinfo *resolved = nullptr;
|
|
std::string err;
|
|
toResolveResult_ = net::DNSResolve(toResolve_, "80", &resolved, err);
|
|
if (resolved)
|
|
net::DNSResolveFree(resolved);
|
|
|
|
resolverState_ = ResolverState::READY;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool AdhocServerScreen::CanComplete(DialogResult result) {
|
|
auto n = GetI18NCategory(I18NCat::NETWORKING);
|
|
|
|
if (result != DR_OK)
|
|
return true;
|
|
|
|
std::string value = editValue_;
|
|
if (lastResolved_ == value) {
|
|
return true;
|
|
}
|
|
|
|
// Currently running.
|
|
if (resolverState_ == ResolverState::PROGRESS)
|
|
return false;
|
|
|
|
std::lock_guard<std::mutex> guard(resolverLock_);
|
|
switch (resolverState_) {
|
|
case ResolverState::PROGRESS:
|
|
case ResolverState::QUIT:
|
|
return false;
|
|
|
|
case ResolverState::QUEUED:
|
|
case ResolverState::WAITING:
|
|
break;
|
|
|
|
case ResolverState::READY:
|
|
if (toResolve_ == value) {
|
|
// Reset the state, nothing there now.
|
|
resolverState_ = ResolverState::WAITING;
|
|
toResolve_.clear();
|
|
lastResolved_ = value;
|
|
lastResolvedResult_ = toResolveResult_;
|
|
|
|
if (lastResolvedResult_) {
|
|
progressView_->SetVisibility(UI::V_GONE);
|
|
} else {
|
|
progressView_->SetText(n->T("Invalid IP or hostname"));
|
|
progressView_->SetLevel(NoticeLevel::ERROR);
|
|
progressView_->SetVisibility(UI::V_VISIBLE);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Throw away that last result, it was for a different value.
|
|
break;
|
|
}
|
|
|
|
resolverState_ = ResolverState::QUEUED;
|
|
toResolve_ = value;
|
|
resolverCond_.notify_one();
|
|
|
|
progressView_->SetText(n->T("Validating address..."));
|
|
progressView_->SetLevel(NoticeLevel::INFO);
|
|
progressView_->SetVisibility(UI::V_VISIBLE);
|
|
|
|
return false;
|
|
}
|
|
|
|
void AdhocServerScreen::OnCompleted(DialogResult result) {
|
|
if (result == DR_OK) {
|
|
*value_ = StripSpaces(editValue_);
|
|
}
|
|
}
|
|
|
|
bool AdhocServerNameIsCustom() {
|
|
for (auto iter : g_Config.vCustomAdhocServerList) {
|
|
if (iter == g_Config.sProAdhocServer) {
|
|
return true;
|
|
}
|
|
}
|
|
for (auto iter : g_Config.vCustomAdhocServerListWithRelay) {
|
|
if (iter == g_Config.sProAdhocServer) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void EditServerName(std::string_view newServerName) {
|
|
newServerName = StripSpaces(newServerName);
|
|
if (newServerName != g_Config.sProAdhocServer) {
|
|
for (auto &name : g_Config.vCustomAdhocServerList) {
|
|
if (name == g_Config.sProAdhocServer) {
|
|
name = newServerName;
|
|
}
|
|
}
|
|
for (auto &name : g_Config.vCustomAdhocServerListWithRelay) {
|
|
if (name == g_Config.sProAdhocServer) {
|
|
name = newServerName;
|
|
}
|
|
}
|
|
g_Config.sProAdhocServer = newServerName;
|
|
}
|
|
}
|
|
|
|
void AskToEditCurrentServer(int requestToken, ScreenManager *screenManager) {
|
|
using namespace UI;
|
|
auto n = GetI18NCategory(I18NCat::NETWORKING);
|
|
|
|
// Choose method depending on platform capabilities.
|
|
if (System_GetPropertyBool(SYSPROP_HAS_TEXT_INPUT_DIALOG)) {
|
|
System_InputBoxGetString(requestToken, n->T("Ad hoc server address"), g_Config.sProAdhocServer, false, [](std::string_view enteredValue, int) {
|
|
EditServerName(enteredValue);
|
|
});
|
|
return;
|
|
}
|
|
static std::string editText;
|
|
editText = g_Config.sProAdhocServer;
|
|
TextEditPopupScreen *popupScreen = new TextEditPopupScreen(&editText, editText, n->T("Ad hoc server address"), 256);
|
|
if (System_GetPropertyBool(SYSPROP_KEYBOARD_IS_SOFT)) {
|
|
popupScreen->SetAlignTop(true);
|
|
}
|
|
popupScreen->OnChange.Add([](EventParams &e) {
|
|
EditServerName(editText);
|
|
});
|
|
screenManager->push(popupScreen);
|
|
}
|