Make wrapper for boost::lexical_cast
[lsnes.git] / src / platform / wxwidgets / window-fileupload.cpp
blob26b070fee8fc45f8deb75884ec7f1a70eb3ab626
1 #include "platform/wxwidgets/platform.hpp"
2 #include "platform/wxwidgets/menu_upload.hpp"
3 #include "core/fileupload.hpp"
4 #include "core/instance.hpp"
5 #include "core/misc.hpp"
6 #include "core/random.hpp"
7 #include "core/rom.hpp"
8 #include "core/project.hpp"
9 #include "core/moviedata.hpp"
10 #include "core/ui-services.hpp"
11 #include "library/directory.hpp"
12 #include "library/skein.hpp"
13 #include "library/zip.hpp"
14 #include "library/json.hpp"
15 #include "library/string.hpp"
16 #include <fstream>
17 #include <iostream>
18 #include <iomanip>
19 #include <wx/wx.h>
20 #include <wx/event.h>
21 #include <wx/control.h>
22 #include <wx/combobox.h>
23 #include <wx/spinctrl.h>
24 #include <boost/iostreams/categories.hpp>
25 #include <boost/iostreams/copy.hpp>
26 #include <boost/iostreams/stream.hpp>
27 #include <boost/iostreams/stream_buffer.hpp>
28 #include <boost/iostreams/filter/symmetric.hpp>
29 #include <boost/iostreams/filter/zlib.hpp>
30 #include <boost/iostreams/filtering_stream.hpp>
31 #include <boost/iostreams/device/back_inserter.hpp>
33 #define NO_GAME_NAME "(default)"
35 std::string pick_file(wxWindow* parent, const std::string& title, const std::string& startdir);
37 namespace
39 const std::string& str_tolower(const std::string& x)
41 static std::map<std::string, std::string> cache;
42 if(!cache.count(x)) {
43 std::ostringstream y;
44 for(auto i : x) {
45 if(i >= 'A' && i <= 'Z')
46 y << (char)(i + 32);
47 else
48 y << (char)i;
50 cache[x] = y.str();
52 return cache[x];
54 inline bool is_prefix(std::string a, std::string b)
56 return (a.length() <= b.length() && b.substr(0, a.length()) == a);
59 bool search_match(const std::string& term, const std::string& name)
61 std::set<std::string> searched;
62 std::vector<std::string> name_words;
63 for(auto i : token_iterator<char>::foreach(name, {" "})) if(i != "") name_words.push_back(str_tolower(i));
64 for(auto i : token_iterator<char>::foreach(term, {" "})) {
65 if(i == "")
66 continue;
67 std::string st = str_tolower(i);
68 if(searched.count(st))
69 continue;
70 for(size_t j = 0; j < name_words.size(); j++) {
71 if(is_prefix(st, name_words[j]))
72 goto out;
74 return false;
75 out:
76 searched.insert(st);
78 return true;
81 class wxwin_gameselect : public wxDialog
83 public:
84 wxwin_gameselect(wxWindow* parent, const std::list<std::string>& _choices, const std::string& dflt,
85 const std::string& system, int x, int y)
86 : wxDialog(parent, wxID_ANY, wxT("lsnes: Pick a game"), wxPoint(x, y)),
87 chosen(dflt), choices(_choices)
89 CHECK_UI_THREAD;
90 wxBoxSizer* top_s = new wxBoxSizer(wxVERTICAL);
91 SetSizer(top_s);
93 top_s->Add(new wxStaticText(this, wxID_ANY, wxT("Search:")), 0, wxGROW);
94 top_s->Add(search = new wxTextCtrl(this, wxID_ANY, wxT("")), 1, wxGROW);
95 top_s->Add(new wxStaticText(this, wxID_ANY, wxT("Game:")), 0, wxGROW);
96 top_s->Add(games = new wxListBox(this, wxID_ANY, wxDefaultPosition, wxSize(400, 300)), 1, wxGROW);
98 search->Connect(wxEVT_COMMAND_TEXT_UPDATED,
99 wxCommandEventHandler(wxwin_gameselect::on_search_type), NULL, this);
100 games->Connect(wxEVT_COMMAND_LISTBOX_SELECTED,
101 wxCommandEventHandler(wxwin_gameselect::on_list_select), NULL, this);
103 wxBoxSizer* pbutton_s = new wxBoxSizer(wxHORIZONTAL);
104 pbutton_s->AddStretchSpacer();
105 pbutton_s->Add(ok = new wxButton(this, wxID_OK, wxT("OK")), 0, wxGROW);
106 pbutton_s->Add(cancel = new wxButton(this, wxID_CANCEL, wxT("Cancel")), 0, wxGROW);
107 ok->Connect(wxEVT_COMMAND_BUTTON_CLICKED,
108 wxCommandEventHandler(wxwin_gameselect::on_ok), NULL, this);
109 cancel->Connect(wxEVT_COMMAND_BUTTON_CLICKED,
110 wxCommandEventHandler(wxwin_gameselect::on_cancel), NULL, this);
111 top_s->Add(pbutton_s, 0, wxGROW);
113 auto dsplit = split_name(dflt);
114 bool wrong_default = (dflt != NO_GAME_NAME && system != "" && dsplit.first != system);
115 if(system != "" && !wrong_default) {
116 //Populate just one system.
117 rsystem = system;
118 oneplat = true;
119 } else {
120 oneplat = false;
122 wxCommandEvent e;
123 on_search_type(e);
124 games->SetStringSelection(towxstring(dflt));
126 std::string get()
128 return chosen;
130 void on_search_type(wxCommandEvent& e)
132 CHECK_UI_THREAD;
133 std::string current;
134 if(games->GetSelection() != wxNOT_FOUND)
135 current = tostdstring(games->GetStringSelection());
136 games->Clear();
137 std::string terms = tostdstring(search->GetValue());
138 for(auto& i : choices) {
139 auto g = split_name(i);
140 if(i != current && i != NO_GAME_NAME) {
141 if(oneplat && g.first != rsystem)
142 continue; //Wrong system.
143 if(!search_match(terms, i))
144 continue; //Doesn't match terms.
146 games->Append(towxstring(i));
148 if(current != "")
149 games->SetStringSelection(towxstring(current));
151 void on_list_select(wxCommandEvent& e)
153 CHECK_UI_THREAD;
154 if(games->GetSelection() != wxNOT_FOUND) {
155 chosen = tostdstring(games->GetStringSelection());
156 ok->Enable(true);
157 } else {
158 ok->Enable(false);
161 void on_ok(wxCommandEvent& e)
163 CHECK_UI_THREAD;
164 EndModal(wxID_OK);
166 void on_cancel(wxCommandEvent& e)
168 CHECK_UI_THREAD;
169 EndModal(wxID_CANCEL);
171 private:
172 std::pair<std::string, std::string> split_name(const std::string& name)
174 if(name == NO_GAME_NAME)
175 return std::make_pair("N/A", NO_GAME_NAME);
176 std::string _name = name;
177 size_t r = _name.find_first_of(" ");
178 if(r >= _name.length())
179 return std::make_pair("???", name);
180 else if(r == 0)
181 return std::make_pair("???", _name.substr(1));
182 else
183 return std::make_pair(_name.substr(0, r), _name.substr(r + 1));
185 std::string chosen;
186 const std::list<std::string>& choices;
187 wxTextCtrl* search;
188 wxListBox* games;
189 wxButton* ok;
190 wxButton* cancel;
191 std::string old_system;
192 bool oneplat;
193 std::string rsystem;
196 class wxeditor_uploadtarget : public wxDialog
198 public:
199 wxeditor_uploadtarget(wxWindow* parent);
200 wxeditor_uploadtarget(wxWindow* parent, upload_menu::upload_entry entry);
201 upload_menu::upload_entry get_entry();
202 void generate_dh25519(wxCommandEvent& e);
203 void on_ok(wxCommandEvent& e);
204 void on_cancel(wxCommandEvent& e);
205 void on_auth_sel(wxCommandEvent& e);
206 void revalidate(wxCommandEvent& e);
207 private:
208 void dh25519_fill_box();
209 void ctor_common();
210 wxButton* ok;
211 wxButton* cancel;
212 wxTextCtrl* name;
213 wxTextCtrl* url;
214 wxComboBox* auth;
215 wxPanel* dh25519_p;
216 wxTextCtrl* dh25519_k;
217 wxButton* dh25519_g;
220 wxeditor_uploadtarget::wxeditor_uploadtarget(wxWindow* parent)
221 : wxDialog(parent, wxID_ANY, towxstring("lsnes: New upload target"))
223 CHECK_UI_THREAD;
224 ctor_common();
225 wxCommandEvent e;
226 on_auth_sel(e);
229 wxeditor_uploadtarget::wxeditor_uploadtarget(wxWindow* parent, upload_menu::upload_entry entry)
230 : wxDialog(parent, wxID_ANY, towxstring("lsnes: Edit upload target: " + entry.name))
232 CHECK_UI_THREAD;
233 ctor_common();
234 name->SetValue(towxstring(entry.name));
235 url->SetValue(towxstring(entry.url));
236 switch(entry.auth) {
237 case upload_menu::upload_entry::AUTH_DH25519:
238 auth->SetSelection(0);
239 wxCommandEvent e;
240 on_auth_sel(e);
241 break;
245 void wxeditor_uploadtarget::dh25519_fill_box()
247 CHECK_UI_THREAD;
248 try {
249 uint8_t rbuf[32];
250 get_dh25519_pubkey(rbuf);
251 char out[65];
252 out[64] = 0;
253 for(unsigned i = 0; i < 32; i++)
254 sprintf(out + 2 * i, "%02x", rbuf[i]);
255 dh25519_k->SetValue(towxstring(out));
256 dh25519_g->Disable();
257 } catch(...) {
258 dh25519_k->SetValue(towxstring("(Not available)"));
259 dh25519_g->Enable();
261 wxCommandEvent e;
262 revalidate(e);
265 void wxeditor_uploadtarget::ctor_common()
267 CHECK_UI_THREAD;
268 ok = NULL;
269 std::vector<wxString> auth_choices;
270 Center();
271 wxBoxSizer* top_s = new wxBoxSizer(wxVERTICAL);
272 SetSizer(top_s);
274 top_s->Add(new wxStaticText(this, wxID_ANY, towxstring("Name")), 0, wxGROW);
275 top_s->Add(name = new wxTextCtrl(this, wxID_ANY, towxstring(""), wxDefaultPosition, wxSize(550, -1)), 0,
276 wxGROW);
277 top_s->Add(new wxStaticText(this, wxID_ANY, towxstring("URL")), 0, wxGROW);
278 top_s->Add(url = new wxTextCtrl(this, wxID_ANY, towxstring(""), wxDefaultPosition, wxSize(550, -1)), 0,
279 wxGROW);
280 name->Connect(wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler(wxeditor_uploadtarget::revalidate), NULL,
281 this);
282 url->Connect(wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler(wxeditor_uploadtarget::revalidate), NULL,
283 this);
285 top_s->Add(new wxStaticText(this, wxID_ANY, towxstring("Authentication")), 0, wxGROW);
286 auth_choices.push_back(towxstring("dh25519"));
287 top_s->Add(auth = new wxComboBox(this, wxID_ANY, auth_choices[0], wxDefaultPosition, wxDefaultSize,
288 auth_choices.size(), &auth_choices[0], wxCB_READONLY), 0, wxGROW);
290 dh25519_p = new wxPanel(this, wxID_ANY);
291 wxBoxSizer* dh25519_s = new wxBoxSizer(wxVERTICAL);
292 dh25519_p->SetSizer(dh25519_s);
293 wxStaticBox* dh25519_b = new wxStaticBox(dh25519_p, wxID_ANY, towxstring("Authentication parameters"));
294 wxStaticBoxSizer* dh25519_s2 = new wxStaticBoxSizer(dh25519_b, wxVERTICAL);
295 top_s->Add(dh25519_p, 0, wxGROW);
296 dh25519_s->Add(dh25519_s2, 0, wxGROW);
297 dh25519_s2->Add(new wxStaticText(dh25519_p, wxID_ANY, towxstring("Key")), 0, wxGROW);
298 dh25519_s2->Add(dh25519_k = new wxTextCtrl(dh25519_p, wxID_ANY, towxstring(""), wxDefaultPosition,
299 wxSize(550, -1), wxTE_READONLY), 0, wxGROW);
300 dh25519_s2->Add(dh25519_g = new wxButton(dh25519_p, wxID_ANY, towxstring("Generate")), 0, wxGROW);
301 dh25519_s->SetSizeHints(dh25519_p);
302 dh25519_fill_box();
304 wxBoxSizer* pbutton_s = new wxBoxSizer(wxHORIZONTAL);
305 pbutton_s->AddStretchSpacer();
306 pbutton_s->Add(ok = new wxButton(this, wxID_OK, wxT("OK")), 0, wxGROW);
307 pbutton_s->Add(cancel = new wxButton(this, wxID_CANCEL, wxT("Cancel")), 0, wxGROW);
308 ok->Connect(wxEVT_COMMAND_BUTTON_CLICKED,
309 wxCommandEventHandler(wxeditor_uploadtarget::on_ok), NULL, this);
310 cancel->Connect(wxEVT_COMMAND_BUTTON_CLICKED,
311 wxCommandEventHandler(wxeditor_uploadtarget::on_cancel), NULL, this);
312 top_s->Add(pbutton_s, 0, wxGROW);
314 wxCommandEvent e;
315 revalidate(e);
317 top_s->SetSizeHints(this);
318 Fit();
321 void wxeditor_uploadtarget::on_ok(wxCommandEvent& e)
323 CHECK_UI_THREAD;
324 EndModal(wxID_OK);
327 void wxeditor_uploadtarget::on_cancel(wxCommandEvent& e)
329 CHECK_UI_THREAD;
330 EndModal(wxID_CANCEL);
333 void wxeditor_uploadtarget::generate_dh25519(wxCommandEvent& e)
335 CHECK_UI_THREAD;
336 uint8_t rbuf[192];
337 try {
338 std::string entropy = pick_text(this, "Enter garbage", "Mash some garbage from keyboard to derive\n"
339 "key from:", "", true);
340 highrandom_256(rbuf + 0);
341 highrandom_256(rbuf + 32);
342 std::vector<char> x;
343 x.resize(entropy.length());
344 std::copy(entropy.begin(), entropy.end(), x.begin());
345 skein::hash h(skein::hash::PIPE_1024, 1024);
346 h.write((uint8_t*)&x[0], x.size());
347 h.read((uint8_t*)rbuf + 64);
349 std::ofstream fp(get_config_path() + "/dh25519.key", std::ios::binary);
350 if(!fp) throw std::runtime_error("Can't open keyfile");
351 #if !defined(_WIN32) && !defined(_WIN64)
352 chmod((get_config_path() + "/dh25519.key").c_str(), 0600);
353 #endif
354 fp.write((char*)rbuf, 192);
355 if(!fp) throw std::runtime_error("Can't write keyfile");
357 skein::zeroize(rbuf, sizeof(rbuf));
358 dh25519_fill_box();
359 } catch(canceled_exception& e) {
360 skein::zeroize(rbuf, sizeof(rbuf));
361 return;
362 } catch(std::exception& e) {
363 skein::zeroize(rbuf, sizeof(rbuf));
364 show_message_ok(this, "Generate keys error", std::string("Error generating keys:") + e.what(),
365 wxICON_EXCLAMATION);
366 return;
370 void wxeditor_uploadtarget::on_auth_sel(wxCommandEvent& e)
372 CHECK_UI_THREAD;
373 dh25519_p->Show(false);
374 switch(auth->GetSelection()) {
375 case 0:
376 dh25519_p->Show(true);
377 break;
379 revalidate(e);
380 Fit();
383 upload_menu::upload_entry wxeditor_uploadtarget::get_entry()
385 CHECK_UI_THREAD;
386 upload_menu::upload_entry ent;
387 ent.name = tostdstring(name->GetValue());
388 ent.url = tostdstring(url->GetValue());
389 ent.auth = upload_menu::upload_entry::AUTH_DH25519;
390 switch(auth->GetSelection()) {
391 case 0:
392 ent.auth = upload_menu::upload_entry::AUTH_DH25519;
393 break;
395 return ent;
398 void wxeditor_uploadtarget::revalidate(wxCommandEvent& e)
400 CHECK_UI_THREAD;
401 bool valid = true;
402 if(!name || (name->GetValue().Length() == 0)) valid = false;
403 std::string URL = url ? tostdstring(url->GetValue()) : "";
404 if(!regex_match("https?://(([!$&'()*+,;=:A-Za-z0-9._~-]|%[0-9A-Fa-f][0-9A-Fa-f])+|\\[v[0-9A-Fa-f]\\."
405 "([!$&'()*+,;=:A-Za-z0-9._~-]|%[0-9A-Fa-f][0-9A-Fa-f])+\\]|\\[[0-9A-Fa-f:]+\\])"
406 "(/([!$&'()*+,;=:A-Za-z0-9._~@-]|%[0-9A-Fa-f][0-9A-Fa-f])+)*", URL)) valid = false;
407 if(!auth || (auth->GetSelection() == 0 && dh25519_g->IsEnabled())) valid = false;
408 if(ok) ok->Enable(valid);
411 class wxeditor_uploadtargets : public wxDialog
413 public:
414 wxeditor_uploadtargets(wxWindow* parent, upload_menu* menu);
415 void on_ok(wxCommandEvent& e);
416 void on_add(wxCommandEvent& e);
417 void on_modify(wxCommandEvent& e);
418 void on_remove(wxCommandEvent& e);
419 void on_list_sel(wxCommandEvent& e);
420 private:
421 void refresh();
422 upload_menu* umenu;
423 std::map<int, unsigned> id_map;
424 wxListBox* list;
425 wxButton* ok;
426 wxButton* add;
427 wxButton* modify;
428 wxButton* _delete;
431 wxeditor_uploadtargets::wxeditor_uploadtargets(wxWindow* parent, upload_menu* menu)
432 : wxDialog(parent, wxID_ANY, towxstring("lsnes: Configure upload targets"), wxDefaultPosition,
433 wxSize(400, 500))
435 CHECK_UI_THREAD;
436 umenu = menu;
437 Center();
438 wxBoxSizer* top_s = new wxBoxSizer(wxVERTICAL);
439 SetSizer(top_s);
441 top_s->Add(list = new wxListBox(this, wxID_ANY), 1, wxGROW);
442 list->Connect(wxEVT_COMMAND_LISTBOX_SELECTED,
443 wxCommandEventHandler(wxeditor_uploadtargets::on_list_sel), NULL, this);
445 wxBoxSizer* pbutton_s = new wxBoxSizer(wxHORIZONTAL);
446 pbutton_s->Add(ok = new wxButton(this, wxID_OK, wxT("OK")), 0, wxGROW);
447 pbutton_s->AddStretchSpacer();
448 pbutton_s->Add(add = new wxButton(this, wxID_ADD, wxT("Add")), 0, wxGROW);
449 pbutton_s->Add(modify = new wxButton(this, wxID_EDIT, wxT("Modify")), 0, wxGROW);
450 pbutton_s->Add(_delete = new wxButton(this, wxID_DELETE, wxT("Delete")), 0, wxGROW);
451 modify->Enable(false);
452 _delete->Enable(false);
453 ok->Connect(wxEVT_COMMAND_BUTTON_CLICKED,
454 wxCommandEventHandler(wxeditor_uploadtargets::on_ok), NULL, this);
455 add->Connect(wxEVT_COMMAND_BUTTON_CLICKED,
456 wxCommandEventHandler(wxeditor_uploadtargets::on_add), NULL, this);
457 modify->Connect(wxEVT_COMMAND_BUTTON_CLICKED,
458 wxCommandEventHandler(wxeditor_uploadtargets::on_modify), NULL, this);
459 _delete->Connect(wxEVT_COMMAND_BUTTON_CLICKED,
460 wxCommandEventHandler(wxeditor_uploadtargets::on_remove), NULL, this);
461 top_s->Add(pbutton_s, 0, wxGROW);
463 refresh();
464 top_s->SetSizeHints(this);
465 Fit();
468 void wxeditor_uploadtargets::on_ok(wxCommandEvent& e)
470 CHECK_UI_THREAD;
471 EndModal(wxID_OK);
474 void wxeditor_uploadtargets::on_add(wxCommandEvent& e)
476 CHECK_UI_THREAD;
477 auto f = new wxeditor_uploadtarget(this);
478 int r = f->ShowModal();
479 if(r == wxID_OK) {
480 unsigned ent = 0;
481 auto used = umenu->entries();
482 while(used.count(ent)) ent++;
483 umenu->configure_entry(ent, f->get_entry());
485 f->Destroy();
486 refresh();
489 void wxeditor_uploadtargets::on_modify(wxCommandEvent& e)
491 CHECK_UI_THREAD;
492 auto s = list->GetSelection();
493 if(s == wxNOT_FOUND) return;
494 if(!id_map.count(s)) return;
495 auto f = new wxeditor_uploadtarget(this, umenu->get_entry(id_map[s]));
496 int r = f->ShowModal();
497 if(r == wxID_OK)
498 umenu->configure_entry(id_map[s], f->get_entry());
499 f->Destroy();
500 refresh();
503 void wxeditor_uploadtargets::on_remove(wxCommandEvent& e)
505 CHECK_UI_THREAD;
506 auto s = list->GetSelection();
507 if(s == wxNOT_FOUND) return;
508 if(!id_map.count(s)) return;
509 unsigned id = id_map[s];
510 umenu->delete_entry(id);
511 refresh();
514 void wxeditor_uploadtargets::on_list_sel(wxCommandEvent& e)
516 CHECK_UI_THREAD;
517 auto s = list->GetSelection();
518 modify->Enable(s != wxNOT_FOUND);
519 _delete->Enable(s != wxNOT_FOUND);
522 void wxeditor_uploadtargets::refresh()
524 CHECK_UI_THREAD;
525 auto ents = umenu->entries();
526 auto sel = list->GetSelection();
527 auto sel_id = id_map.count(sel) ? id_map[sel] : 0xFFFFFFFFU;
529 list->Clear();
530 id_map.clear();
531 int num = 0;
532 for(auto i : ents) {
533 auto ent = umenu->get_entry(i);
534 list->Append(towxstring(ent.name));
535 id_map[num++] = i;
538 //Try to keep selection.
539 if(sel_id != 0xFFFFFFFFU) {
540 int x = wxNOT_FOUND;
541 for(auto i : id_map)
542 if(i.second == sel_id)
543 x = i.first;
544 if(x != wxNOT_FOUND)
545 list->SetSelection(x);
546 } else if(sel < (signed)list->GetCount())
547 list->SetSelection(sel);
550 class wxeditor_uploaddialog : public wxDialog
552 public:
553 wxeditor_uploaddialog(wxWindow* parent, emulator_instance& inst, upload_menu::upload_entry entry);
554 void on_ok(wxCommandEvent& e);
555 void on_cancel(wxCommandEvent& e);
556 void on_source_sel(wxCommandEvent& e);
557 void on_file_sel(wxCommandEvent& e);
558 void on_wclose(wxCloseEvent& e);
559 void timer_tick();
560 void on_game_sel(wxCommandEvent& e);
561 private:
562 struct _timer : public wxTimer
564 _timer(wxeditor_uploaddialog* _dialog) { dialog = _dialog; start(); }
565 void start() { Start(500); }
566 void stop() { Stop(); }
567 void Notify()
569 dialog->timer_tick();
571 wxeditor_uploaddialog* dialog;
572 }* timer;
573 struct _games_output_handler : public http_request::output_handler {
574 ~_games_output_handler()
577 void header(const std::string& name, const std::string& cotent)
579 //No-op.
581 void write(const char* source, size_t srcsize)
583 std::string x(source, srcsize);
584 while(x.find_first_of("\n") < x.length()) {
585 size_t split = x.find_first_of("\n");
586 std::string line = x.substr(0, split);
587 x = x.substr(split + 1);
588 incomplete_line += line;
589 while(incomplete_line.length() > 0 &&
590 incomplete_line[incomplete_line.length() - 1] == '\r')
591 incomplete_line = incomplete_line.substr(0, incomplete_line.length() - 1);
592 choices.insert(incomplete_line);
593 incomplete_line = "";
595 if(x != "") incomplete_line += x;
597 void flush()
599 if(incomplete_line != "") choices.insert(incomplete_line);
601 std::string incomplete_line;
602 std::set<std::string> choices;
603 } games_output_handler;
604 emulator_instance& inst;
605 wxTextCtrl* status;
606 wxTextCtrl* filename;
607 wxTextCtrl* title;
608 wxTextCtrl* description;
609 std::list<std::string> games_list;
610 wxStaticText* game;
611 wxButton* game_sel_button;
612 wxRadioButton* current;
613 wxRadioButton* file;
614 wxTextCtrl* ufilename;
615 wxButton* file_select;
616 wxGauge* progress;
617 wxCheckBox* hidden;
618 wxButton* ok;
619 wxButton* cancel;
620 file_upload* upload;
621 http_async_request* games_req;
622 upload_menu::upload_entry _entry;
625 wxeditor_uploaddialog::wxeditor_uploaddialog(wxWindow* parent, emulator_instance& _inst,
626 upload_menu::upload_entry entry)
627 : wxDialog(parent, wxID_ANY, towxstring("lsnes: Upload file: " + entry.name), wxDefaultPosition,
628 wxSize(-1, -1)), inst(_inst)
630 CHECK_UI_THREAD;
631 _entry = entry;
632 upload = NULL;
633 Centre();
634 wxBoxSizer* top_s = new wxBoxSizer(wxVERTICAL);
635 SetSizer(top_s);
637 Connect(wxEVT_CLOSE_WINDOW, wxCloseEventHandler(wxeditor_uploaddialog::on_wclose), NULL, this);
639 top_s->Add(new wxStaticText(this, wxID_ANY, wxT("Filename:")), 0, wxGROW);
640 top_s->Add(filename = new wxTextCtrl(this, wxID_ANY, wxT(""), wxDefaultPosition, wxSize(550, -1)), 0,
641 wxGROW);
642 top_s->Add(new wxStaticText(this, wxID_ANY, wxT("Title:")), 0, wxGROW);
643 top_s->Add(title = new wxTextCtrl(this, wxID_ANY, wxT(""), wxDefaultPosition, wxSize(550, -1)), 0, wxGROW);
644 top_s->Add(new wxStaticText(this, wxID_ANY, wxT("Description:")), 0, wxGROW);
645 top_s->Add(description = new wxTextCtrl(this, wxID_ANY, wxT(""), wxDefaultPosition, wxSize(550, 300),
646 wxTE_MULTILINE), 0, wxGROW);
647 wxBoxSizer* game_s = new wxBoxSizer(wxHORIZONTAL);
648 game_s->Add(new wxStaticText(this, wxID_ANY, wxT("Game:")), 0, wxGROW);
649 game_s->Add(game = new wxStaticText(this, wxID_ANY, wxT(NO_GAME_NAME)), 1, wxGROW);
650 game_s->Add(game_sel_button = new wxButton(this, wxID_ANY, wxT("Select")), 0, wxGROW);
651 top_s->Add(game_s, 0, wxGROW);
652 games_list.push_back(NO_GAME_NAME);
653 game_sel_button->Enable(false);
654 game_sel_button->Connect(wxEVT_COMMAND_BUTTON_CLICKED,
655 wxCommandEventHandler(wxeditor_uploaddialog::on_game_sel), NULL, this);
656 top_s->Add(hidden = new wxCheckBox(this, wxID_ANY, wxT("Hidden")), 0, wxGROW);
658 top_s->Add(current = new wxRadioButton(this, wxID_ANY, wxT("Current movie"), wxDefaultPosition, wxDefaultSize,
659 wxRB_GROUP), 0, wxGROW);
660 top_s->Add(file = new wxRadioButton(this, wxID_ANY, wxT("Specified file:")), 0, wxGROW);
661 current->Connect(wxEVT_COMMAND_RADIOBUTTON_SELECTED,
662 wxCommandEventHandler(wxeditor_uploaddialog::on_source_sel), NULL, this);
663 file->Connect(wxEVT_COMMAND_RADIOBUTTON_SELECTED,
664 wxCommandEventHandler(wxeditor_uploaddialog::on_source_sel), NULL, this);
665 if(!UI_has_movie(inst)) {
666 current->Enable(false);
667 file->SetValue(true);
670 wxBoxSizer* file_s = new wxBoxSizer(wxHORIZONTAL);
671 file_s->Add(ufilename = new wxTextCtrl(this, wxID_ANY, wxT("")), 1, wxGROW);
672 file_s->Add(file_select = new wxButton(this, wxID_ANY, wxT("...")), 0, wxGROW);
673 top_s->Add(file_s, 0, wxGROW);
674 file_select->Connect(wxEVT_COMMAND_BUTTON_CLICKED,
675 wxCommandEventHandler(wxeditor_uploaddialog::on_file_sel), NULL, this);
676 ufilename->Enable(file->GetValue());
677 file_select->Enable(file->GetValue());
679 top_s->Add(status = new wxTextCtrl(this, wxID_ANY, wxT(""), wxDefaultPosition, wxSize(550, 300),
680 wxTE_READONLY | wxTE_MULTILINE), 0, wxGROW);
681 top_s->Add(progress = new wxGauge(this, wxID_ANY, 1000000, wxDefaultPosition, wxSize(-1, 15),
682 wxGA_HORIZONTAL), 0, wxGROW);
684 status->AppendText(wxT("Obtaining list of games...\n"));
685 games_req = new http_async_request();
686 games_req->verb = "GET";
687 games_req->url = entry.url + "/games";
688 games_req->ihandler = NULL;
689 games_req->ohandler = &games_output_handler;
690 games_req->lauch_async();
692 timer = new _timer(this);
694 wxBoxSizer* pbutton_s = new wxBoxSizer(wxHORIZONTAL);
695 pbutton_s->AddStretchSpacer();
696 pbutton_s->Add(ok = new wxButton(this, wxID_OK, wxT("Upload")), 0, wxGROW);
697 ok->Connect(wxEVT_COMMAND_BUTTON_CLICKED,
698 wxCommandEventHandler(wxeditor_uploaddialog::on_ok), NULL, this);
699 pbutton_s->Add(cancel = new wxButton(this, wxID_OK, wxT("Cancel")), 0, wxGROW);
700 cancel->Connect(wxEVT_COMMAND_BUTTON_CLICKED,
701 wxCommandEventHandler(wxeditor_uploaddialog::on_cancel), NULL, this);
702 top_s->Add(pbutton_s, 0, wxGROW);
703 top_s->SetSizeHints(this);
704 Fit();
707 void wxeditor_uploaddialog::timer_tick()
709 CHECK_UI_THREAD;
710 if(upload) {
711 for(auto i : upload->get_messages()) {
712 status->AppendText(towxstring(i + "\n"));
714 if(upload->finished) {
715 delete upload;
716 upload = NULL;
717 cancel->SetLabel(wxT("Close"));
718 } else {
719 auto prog = upload->get_progress_ppm();
720 if(prog < 0)
721 progress->Pulse();
722 else {
723 progress->SetRange(1000000);
724 progress->SetValue(prog);
727 } else if(games_req) {
728 progress->Pulse();
729 if(games_req->finished) {
730 std::string msg;
731 games_output_handler.flush();
732 if(games_req->errormsg != "") {
733 msg = (stringfmt() << "Error getting list of games: " << (games_req->errormsg)).str();
734 } else if(games_req->http_code != 200) {
735 msg = (stringfmt() << "Got unexpected HTTP status " << (games_req->http_code)).str();
736 } else {
737 for(auto i : games_output_handler.choices)
738 games_list.push_back(i);
739 if(games_list.size() > 1)
740 game_sel_button->Enable(true);
741 msg = "Got list of games.";
743 status->AppendText(towxstring(msg + "\n"));
744 delete games_req;
745 games_req = NULL;
746 wxCommandEvent e;
747 on_source_sel(e);
749 } else {
750 if(progress) {
751 progress->SetRange(1000000);
752 progress->SetValue(0);
757 void wxeditor_uploaddialog::on_ok(wxCommandEvent& e)
759 CHECK_UI_THREAD;
760 if(file->GetValue() && ufilename->GetValue().Length() == 0) return;
761 std::string fn = tostdstring(filename->GetValue());
762 std::vector<char> content;
763 if(file->GetValue()) {
764 if(fn == "") {
765 std::string name = tostdstring(ufilename->GetValue());
766 auto r = regex(".*/([^/]+)", name);
767 if(r) name = r[1];
768 filename->SetValue(towxstring(name));
770 boost::iostreams::back_insert_device<std::vector<char>> rd(content);
771 std::ifstream in(tostdstring(ufilename->GetValue()), std::ios::binary);
772 if(!in) {
773 status->AppendText(towxstring("Can't open '" + tostdstring(ufilename->GetValue()) + "'\n"));
774 return;
776 boost::iostreams::copy(in, rd);
777 } else {
778 if(fn.length() < 6 || fn.substr(fn.length() - 5) != ".lsmv")
779 filename->SetValue(towxstring(fn + ".lsmv"));
780 std::ostringstream stream;
781 UI_save_movie(inst, stream);
782 std::string _stream = stream.str();
783 content = std::vector<char>(_stream.begin(), _stream.end());
785 ok->Enable(false);
786 upload = new file_upload();
787 upload->base_url = _entry.url;
788 upload->content = content;
789 upload->filename = tostdstring(filename->GetValue());
790 upload->title = tostdstring(title->GetValue());
791 upload->description = tostdstring(description->GetValue());
792 upload->gamename = tostdstring(game->GetLabel());
793 upload->hidden = hidden->GetValue();
794 upload->do_async();
797 void wxeditor_uploaddialog::on_source_sel(wxCommandEvent& e)
799 CHECK_UI_THREAD;
800 ufilename->Enable(file->GetValue());
801 file_select->Enable(file->GetValue());
802 if(!games_req) {
803 if(current->GetValue()) {
804 auto g = UI_lookup_platform_and_game(inst);
805 std::string plat = g.first + " ";
806 std::string curgame = g.second;
807 size_t platlen = plat.length();
808 std::string c = tostdstring(game->GetLabel());
809 std::string fullname = plat + curgame;
810 //The rules here are:
811 //If there is fullname among games, select that.
812 //If not and the previous selection has the same system, keep it.
813 //Otherwise select (default).
814 bool done = false;
815 for(auto& i : games_list) {
816 if(i == fullname) {
817 game->SetLabel(towxstring(fullname));
818 done = true;
819 break;
822 if(!done) {
823 if(c.substr(0, platlen) == plat)
824 done = true; //Keep.
826 if(!done)
827 game->SetLabel(towxstring(NO_GAME_NAME));
832 void wxeditor_uploaddialog::on_file_sel(wxCommandEvent& e)
834 CHECK_UI_THREAD;
835 std::string f;
836 try {
837 f = pick_file(this, "Pick file to send", ".");
838 } catch(canceled_exception& e) {
839 return;
841 ufilename->SetValue(towxstring(f));
844 void wxeditor_uploaddialog::on_game_sel(wxCommandEvent& e)
846 CHECK_UI_THREAD;
847 auto pos = game_sel_button->GetScreenPosition();
848 std::string system;
849 if(current->GetValue()) {
850 system = UI_lookup_platform_and_game(inst).first;
852 wxwin_gameselect* gs = new wxwin_gameselect(this, games_list, tostdstring(game->GetLabel()), system,
853 pos.x, pos.y);
854 if(gs->ShowModal() != wxID_OK) {
855 delete gs;
856 return;
858 game->SetLabel(towxstring(gs->get()));
859 return;
862 void wxeditor_uploaddialog::on_cancel(wxCommandEvent& e)
864 CHECK_UI_THREAD;
865 if(games_req) {
866 games_req->cancel();
867 while(!games_req->finished)
868 usleep(100000);
869 delete games_req;
871 if(upload) {
872 upload->cancel();
873 while(!upload->finished)
874 usleep(100000);
875 delete upload;
877 timer->stop();
878 delete timer;
879 EndModal(wxID_CANCEL);
882 void wxeditor_uploaddialog::on_wclose(wxCloseEvent& e)
884 CHECK_UI_THREAD;
885 wxCommandEvent e2;
886 on_cancel(e2);
891 upload_menu::upload_menu(wxWindow* win, emulator_instance& _inst, int wxid_low, int wxid_high)
892 : inst(_inst)
894 CHECK_UI_THREAD;
895 pwin = win;
896 wxid_range_low = wxid_low;
897 wxid_range_high = wxid_high;
898 Append(wxid_range_high, towxstring("Configure..."));
899 win->Connect(wxid_low, wxid_high, wxEVT_COMMAND_MENU_SELECTED,
900 wxCommandEventHandler(upload_menu::on_select), NULL, this);
902 std::ifstream in(get_config_path() + "/upload.cfg");
903 std::string line;
904 unsigned num = 0;
905 while(std::getline(in, line)) {
906 upload_entry entry;
907 try {
908 JSON::node n(line);
909 if(!n.field_exists("name") || n.type_of("name") != JSON::string)
910 continue;
911 if(!n.field_exists("url") || n.type_of("url") != JSON::string)
912 continue;
913 if(!n.field_exists("auth") || n.type_of("auth") != JSON::string)
914 continue;
915 entry.name = n["name"].as_string8();
916 entry.url = n["url"].as_string8();
917 std::string auth = n["auth"].as_string8();
918 if(auth == "dh25519")
919 entry.auth = upload_entry::AUTH_DH25519;
920 else
921 continue;
922 } catch(...) {
923 continue;
925 if(num == 0)
926 PrependSeparator();
927 entry.item = Prepend(wxid_range_low + num, towxstring(entry.name + "..."));
928 destinations[wxid_range_low + num] = entry;
929 num++;
934 upload_menu::~upload_menu()
938 void upload_menu::save()
940 std::string base = get_config_path() + "/upload.cfg";
942 std::ofstream out(base + ".tmp");
943 if(!out)
944 return;
945 for(auto i : destinations) {
946 upload_entry entry = i.second;
947 JSON::node n(JSON::object);
948 n["name"] = JSON::string(entry.name);
949 n["url"] = JSON::string(entry.url);
950 switch(entry.auth) {
951 case upload_entry::AUTH_DH25519:
952 n["auth"] = JSON::string("dh25519");
953 break;
955 out << n.serialize() << std::endl;
957 if(!out)
958 return;
960 directory::rename_overwrite((base + ".tmp").c_str(), base.c_str());
963 void upload_menu::configure_entry(unsigned num, struct upload_entry entry)
965 CHECK_UI_THREAD;
966 if(destinations.count(wxid_range_low + num)) {
967 //Reconfigure.
968 auto tmp = destinations[wxid_range_low + num].item;
969 destinations[wxid_range_low + num] = entry;
970 destinations[wxid_range_low + num].item = tmp;
971 destinations[wxid_range_low + num].item->SetItemLabel(towxstring(entry.name + "..."));
972 } else {
973 //New entry.
974 if(destinations.size() == 0)
975 PrependSeparator();
976 entry.item = Prepend(wxid_range_low + num, towxstring(entry.name + "..."));
977 destinations[wxid_range_low + num] = entry;
979 save();
982 std::set<unsigned> upload_menu::entries()
984 std::set<unsigned> r;
985 for(auto i : destinations)
986 r.insert(i.first - wxid_range_low);
987 return r;
990 upload_menu::upload_entry upload_menu::get_entry(unsigned num)
992 if(destinations.count(wxid_range_low + num))
993 return destinations[wxid_range_low + num];
994 else
995 throw std::runtime_error("No such upload target");
998 void upload_menu::delete_entry(unsigned num)
1000 CHECK_UI_THREAD;
1001 if(destinations.count(wxid_range_low + num)) {
1002 Delete(destinations[wxid_range_low + num].item);
1003 destinations.erase(wxid_range_low + num);
1005 save();
1008 void upload_menu::on_select(wxCommandEvent& e)
1010 CHECK_UI_THREAD;
1011 int id = e.GetId();
1012 modal_pause_holder hld;
1013 try {
1014 wxDialog* f;
1015 if(id == wxid_range_high) {
1016 f = new wxeditor_uploadtargets(pwin, this);
1017 } else if(destinations.count(id)) {
1018 f = new wxeditor_uploaddialog(pwin, inst, destinations[id]);
1019 } else
1020 return;
1021 f->ShowModal();
1022 f->Destroy();
1023 } catch(canceled_exception& e) {
1024 throw;
1025 } catch(...) {
1026 throw canceled_exception();