#include #include #include namespace ui { extern MainApplication* mainApp; namespace { constexpr int ListX = theme::space::lg; constexpr int ListW = 760; constexpr int PanelX = ListX + ListW + theme::space::xl; constexpr int PanelW = theme::layout::ScreenW - PanelX - theme::space::lg; constexpr int ContentY = theme::layout::ContentTop + theme::space::md; constexpr int ContentH = theme::layout::ContentH - 2 * theme::space::md; constexpr int BtnH = 56; constexpr int BtnW = PanelW - 2 * theme::space::lg; } // namespace TitlesLayout::TitlesLayout() { using namespace theme; this->titlesMenu = pu::ui::elm::Menu::New(ListX, ContentY, ListW, color::BgBase, color::BgSurface2, 88, 6); this->titlesMenu->SetScrollbarColor(color::Primary); this->titlesMenu->SetItemsFocusColor(color::BgSurface2); this->titlesMenu->SetOnSelectionChanged([this]() { this->refreshPanel(); }); this->SetBackgroundColor(color::BgBase); this->Add(this->titlesMenu); this->panelBg = pu::ui::elm::Rectangle::New(PanelX, ContentY, PanelW, ContentH, color::BgSurface, radius::lg); this->Add(this->panelBg); this->panelTitle = pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + space::lg, ""); this->panelTitle->SetFont(type::font(type::Title)); this->panelTitle->SetColor(color::TextPrimary); this->Add(this->panelTitle); this->panelHint = pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + space::lg + 48, "Pick an action:"); this->panelHint->SetFont(type::font(type::Body)); this->panelHint->SetColor(color::TextSecondary); this->Add(this->panelHint); int btnY = ContentY + 200; this->btnTransferBg = pu::ui::elm::Rectangle::New(PanelX + space::lg, btnY, BtnW, BtnH, color::BgSurface2, radius::md); this->Add(this->btnTransferBg); this->btnTransferText = pu::ui::elm::TextBlock::New(PanelX + space::lg + space::md, btnY + 14, "Transfer to another device"); this->btnTransferText->SetFont(type::font(type::Body)); this->btnTransferText->SetColor(color::TextSecondary); this->Add(this->btnTransferText); int btnY2 = btnY + BtnH + space::md; this->btnReceiveBg = pu::ui::elm::Rectangle::New(PanelX + space::lg, btnY2, BtnW, BtnH, color::BgSurface2, radius::md); this->Add(this->btnReceiveBg); this->btnReceiveText = pu::ui::elm::TextBlock::New(PanelX + space::lg + space::md, btnY2 + 14, "Receive from another device"); this->btnReceiveText->SetFont(type::font(type::Body)); this->btnReceiveText->SetColor(color::TextSecondary); this->Add(this->btnReceiveText); this->panelFooter = pu::ui::elm::TextBlock::New( PanelX + space::lg, ContentY + ContentH - space::lg - 18, "Save data only"); this->panelFooter->SetFont(type::font(type::Caption)); this->panelFooter->SetColor(color::TextMuted); this->Add(this->panelFooter); this->emptyText = pu::ui::elm::TextBlock::New(ListX + ListW / 2 - 280, ContentY + ContentH / 2 - 40, "No save data on this profile"); this->emptyText->SetFont(type::font(type::Display)); this->emptyText->SetColor(color::TextPrimary); this->emptyText->SetVisible(false); this->Add(this->emptyText); this->emptySub = pu::ui::elm::TextBlock::New(ListX + ListW / 2 - 220, ContentY + ContentH / 2 + 16, "Play something first, then come back."); this->emptySub->SetFont(type::font(type::Body)); this->emptySub->SetColor(color::TextMuted); this->emptySub->SetVisible(false); this->Add(this->emptySub); this->header = std::make_unique(this, "Save Transfer"); this->hints = std::make_unique(this); this->updateHints(); } void TitlesLayout::InitTitles(AccountUid uid) { using namespace theme; this->current_uid = uid; auto it = this->menuCache.find(uid); std::vector* items; if (it != this->menuCache.end()) { items = &it->second; } else { std::vector built; for (size_t i = 0; i < getTitleCount(uid); i++) { Title title; getTitle(title, uid, i); auto titleItem = pu::ui::elm::MenuItem::New(title.name().c_str()); titleItem->SetColor(color::TextPrimary); built.push_back(titleItem); } auto inserted = this->menuCache.emplace(uid, std::move(built)); items = &inserted.first->second; } this->titlesMenu->ClearItems(); for (auto& item : *items) { this->titlesMenu->AddItem(item); } this->titlesMenu->SetSelectedIndex(0); const bool empty = items->empty(); this->titlesMenu->SetVisible(!empty); this->panelBg->SetVisible(!empty); this->panelTitle->SetVisible(!empty); this->panelHint->SetVisible(!empty); this->btnTransferBg->SetVisible(!empty); this->btnTransferText->SetVisible(!empty); this->btnReceiveBg->SetVisible(!empty); this->btnReceiveText->SetVisible(!empty); this->panelFooter->SetVisible(!empty); this->emptyText->SetVisible(empty); this->emptySub->SetVisible(empty); this->focus = TitlesFocus::List; this->action = TitlesAction::Transfer; this->refreshPanel(); this->refreshButtons(); this->updateHints(); this->header->SetUser(uid, account::username(uid)); } void TitlesLayout::refreshPanel() { if (this->titlesMenu->GetItems().empty()) return; int idx = this->titlesMenu->GetSelectedIndex(); Title title; getTitle(title, this->current_uid, idx); this->panelTitle->SetText(string_utils::elide(title.name(), 24)); } void TitlesLayout::refreshButtons() { using namespace theme; const bool active = (focus == TitlesFocus::Actions); if (active && action == TitlesAction::Transfer) { this->btnTransferBg->SetColor(color::Primary); this->btnTransferText->SetColor(color::TextPrimary); this->btnReceiveBg->SetColor(color::BgSurface2); this->btnReceiveText->SetColor(color::TextSecondary); } else if (active && action == TitlesAction::Receive) { this->btnTransferBg->SetColor(color::BgSurface2); this->btnTransferText->SetColor(color::TextSecondary); this->btnReceiveBg->SetColor(color::Accent); this->btnReceiveText->SetColor(color::BgBase); } else { this->btnTransferBg->SetColor(color::BgSurface2); this->btnTransferText->SetColor(color::TextSecondary); this->btnReceiveBg->SetColor(color::BgSurface2); this->btnReceiveText->SetColor(color::TextSecondary); } } void TitlesLayout::updateHints() { if (focus == TitlesFocus::List) { this->hints->SetHints({{"A", "Choose action"}, {"B", "Back"}, {"+", "Quit"}}); } else { this->hints->SetHints({{"A", "Confirm"}, {"B", "Back"}}); } } void TitlesLayout::runTransfer(int index, Title& title) { (void)title; auto ovl = TransferOverlay::New("Transferring save data..."); this->titlesMenu->SetVisible(false); mainApp->StartOverlay(ovl); this->LockInput(); if (mainApp->transfer.startSend((size_t)index, this->current_uid) != 0) { mainApp->EndOverlay(); this->titlesMenu->SetVisible(true); this->UnlockInput(); mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true); return; } while (!mainApp->transfer.isSendDone()) { ovl->SetStatus(mainApp->transfer.sendStatusText()); ovl->SetProgressVisible(mainApp->transfer.isSendProgressKnown()); ovl->SetProgress(mainApp->transfer.sendProgress()); mainApp->CallForRender(); if (mainApp->GetButtonsDown() & HidNpadButton_B) { mainApp->transfer.cancelSend(); } svcSleepThread(16666666LL); } mainApp->EndOverlay(); this->titlesMenu->SetVisible(true); this->UnlockInput(); if (mainApp->transfer.isSendConnectionFailed()) { mainApp->CreateShowDialog("Transfer", mainApp->transfer.sendFailReason(), {"OK"}, true); } else if (mainApp->transfer.isSendCancelled()) { mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true); } else { mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true); } } void TitlesLayout::runReceive(int index, Title& title) { if (mainApp->transfer.startReceive((size_t)index, this->current_uid, title.name()) != 0) { mainApp->CreateShowDialog("Receive", "Failed to start receiver.\nCheck network connection.", {"OK"}, true); return; } auto ovl = TransferOverlay::New("Receiving save data..."); this->titlesMenu->SetVisible(false); mainApp->StartOverlay(ovl); this->LockInput(); while (!mainApp->transfer.isReceiveDone()) { ovl->SetStatus(mainApp->transfer.receiveStatusText()); ovl->SetProgress(mainApp->transfer.receiveProgress()); mainApp->CallForRender(); if (mainApp->GetButtonsDown() & HidNpadButton_B) { mainApp->transfer.cancelReceive(); } svcSleepThread(16666666LL); } mainApp->EndOverlay(); this->titlesMenu->SetVisible(true); this->UnlockInput(); if (mainApp->transfer.isReceiveCancelled()) { mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true); } else if (mainApp->transfer.restoreSucceeded()) { mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true); } else { mainApp->CreateShowDialog("Receive", "Restore failed:\n" + mainApp->transfer.restoreError(), {"OK"}, true); } } void TitlesLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { (void)Up; (void)Held; (void)Pos; if (m_inputLocked) return; if (Down & HidNpadButton_Plus) { mainApp->transfer.cancelSend(); mainApp->transfer.cancelReceive(); mainApp->Close(); return; } if (focus == TitlesFocus::List) { if (Down & HidNpadButton_B) { this->header->SetUser(std::nullopt, ""); mainApp->LoadLayout(mainApp->users_layout); return; } if (Down & HidNpadButton_A) { if (this->titlesMenu->GetItems().empty()) return; this->lockedListIndex = this->titlesMenu->GetSelectedIndex(); this->focus = TitlesFocus::Actions; this->action = TitlesAction::Transfer; this->refreshButtons(); this->updateHints(); return; } } else { if (this->titlesMenu->GetSelectedIndex() != this->lockedListIndex) { this->titlesMenu->SetSelectedIndex(this->lockedListIndex); } if (Down & HidNpadButton_B) { this->focus = TitlesFocus::List; this->refreshButtons(); this->updateHints(); return; } if (Down & (HidNpadButton_Up | HidNpadButton_Down | HidNpadButton_StickLUp | HidNpadButton_StickLDown)) { this->action = (action == TitlesAction::Transfer) ? TitlesAction::Receive : TitlesAction::Transfer; this->refreshButtons(); return; } if (Down & HidNpadButton_A) { int idx = this->titlesMenu->GetSelectedIndex(); Title title; getTitle(title, this->current_uid, idx); TitlesAction chosen = action; this->focus = TitlesFocus::List; this->refreshButtons(); this->updateHints(); if (chosen == TitlesAction::Transfer) { this->runTransfer(idx, title); } else { this->runReceive(idx, title); } } } } } // namespace ui