1
0

MainPage.cpp 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. #include "pch.h"
  2. #include "MainPage.h"
  3. #include "MainPage.g.cpp"
  4. #include <filesystem>
  5. #include <winrt/Windows.ApplicationModel.Core.h>
  6. #include <winrt/Windows.UI.ViewManagement.h>
  7. #include <winrt/Windows.UI.Xaml.Media.h>
  8. #include "Model\Netif.h"
  9. namespace winrt::Maple_App::implementation
  10. {
  11. using namespace std::chrono_literals;
  12. using namespace winrt::Windows::UI::ViewManagement;
  13. using namespace winrt::Windows::UI::Xaml;
  14. using namespace winrt::Windows::UI::Xaml::Media;
  15. std::string getNormalizedExtentionFromPath(const winrt::hstring& path) {
  16. auto ext = std::filesystem::path(std::wstring_view(path)).extension().string();
  17. std::transform(ext.begin(), ext.end(), ext.begin(), [](const auto ch) {
  18. return static_cast<char>(std::tolower(ch));
  19. });
  20. return ext;
  21. }
  22. MainPage::MainPage()
  23. {
  24. InitializeComponent();
  25. const auto coreTitleBar{ CoreApplication::GetCurrentView().TitleBar() };
  26. const auto window{ Window::Current() };
  27. coreTitleBar.ExtendViewIntoTitleBar(true);
  28. window.SetTitleBar(CustomTitleBar());
  29. coreTitleBar.LayoutMetricsChanged({ this,&MainPage::CoreTitleBar_LayoutMetricsChanged });
  30. window.Activated({ this, &MainPage::CoreWindow_Activated });
  31. }
  32. void MainPage::CoreTitleBar_LayoutMetricsChanged(CoreApplicationViewTitleBar const& coreTitleBar, IInspectable const&)
  33. {
  34. LeftPaddingColumn().Width(GridLength{ .Value = coreTitleBar.SystemOverlayLeftInset(), .GridUnitType = GridUnitType::Pixel });
  35. RightPaddingColumn().Width(GridLength{ .Value = coreTitleBar.SystemOverlayRightInset(), .GridUnitType = GridUnitType::Pixel });
  36. }
  37. void MainPage::CoreWindow_Activated(IInspectable const&, WindowActivatedEventArgs const& args)
  38. {
  39. UISettings settings{};
  40. if (args.WindowActivationState() == CoreWindowActivationState::Deactivated)
  41. {
  42. AppTitleTextBlock().Foreground(
  43. SolidColorBrush{ settings.UIElementColor(UIElementType::GrayText) });
  44. }
  45. else
  46. {
  47. AppTitleTextBlock().Foreground(
  48. SolidColorBrush{ settings.GetColorValue(UIColorType::Foreground) });
  49. }
  50. }
  51. DependencyProperty MainPage::ConfigItemsProperty()
  52. {
  53. return m_configItemsProperty;
  54. }
  55. IObservableVector<Maple_App::ConfigViewModel> MainPage::ConfigItems()
  56. {
  57. return GetValue(m_configItemsProperty).as<IObservableVector<Maple_App::ConfigViewModel>>();
  58. }
  59. void MainPage::Page_Loaded(IInspectable const&, RoutedEventArgs const&)
  60. {
  61. const auto _ = LoadConfigs();
  62. NavigationManager = SystemNavigationManager::GetForCurrentView();
  63. NavigationManager.AppViewBackButtonVisibility(MainSplitView().IsPaneOpen()
  64. ? AppViewBackButtonVisibility::Collapsed
  65. : AppViewBackButtonVisibility::Visible);
  66. const auto weakThis = get_weak();
  67. NavigationManager.BackRequested([weakThis](const auto&, const auto&) {
  68. if (const auto self{ weakThis.get() }) {
  69. self->MainSplitView().IsPaneOpen(true);
  70. const auto currentVisibility = NavigationManager.AppViewBackButtonVisibility();
  71. if (currentVisibility == AppViewBackButtonVisibility::Visible) {
  72. NavigationManager.AppViewBackButtonVisibility(AppViewBackButtonVisibility::Disabled);
  73. }
  74. }
  75. });
  76. StartConnectionCheck();
  77. }
  78. IAsyncAction MainPage::NotifyUser(const hstring& message) {
  79. ContentDialog dialog;
  80. dialog.Content(box_value(message));
  81. dialog.PrimaryButtonCommand(StandardUICommand{ StandardUICommandKind::Close });
  82. co_await dialog.ShowAsync();
  83. }
  84. IAsyncOperation<IStorageFolder> MainPage::InitializeConfigFolder()
  85. {
  86. const auto& appData = ApplicationData::Current();
  87. const auto& localFolder = appData.LocalFolder();
  88. auto configItem = co_await localFolder.TryGetItemAsync(L"config");
  89. if (configItem == nullptr || configItem.IsOfType(StorageItemTypes::File)) {
  90. configItem = co_await localFolder.CreateFolderAsync(L"config", CreationCollisionOption::ReplaceExisting);
  91. }
  92. co_return configItem.as<IStorageFolder>();
  93. }
  94. IAsyncOperation<StorageFile> MainPage::CopyDefaultConfig(const IStorageFolder& configFolder, const hstring& desiredName)
  95. {
  96. const auto& defaultConfigSrc = co_await StorageFile::GetFileFromApplicationUriAsync(Uri{ L"ms-appx:///Config/default.conf" });
  97. co_return co_await defaultConfigSrc.CopyAsync(configFolder, desiredName, NameCollisionOption::GenerateUniqueName);
  98. }
  99. IAsyncOperation<StorageFile> MainPage::CopyDefaultJsonConfig(const IStorageFolder& configFolder, const hstring& desiredName)
  100. {
  101. const auto& defaultConfigSrc = co_await StorageFile::GetFileFromApplicationUriAsync(Uri{ L"ms-appx:///Config/default.json" });
  102. co_return co_await defaultConfigSrc.CopyAsync(configFolder, desiredName, NameCollisionOption::GenerateUniqueName);
  103. }
  104. IAsyncAction MainPage::LoadConfigs()
  105. {
  106. const auto lifetime = get_strong();
  107. const auto& appData = ApplicationData::Current();
  108. m_configFolder = co_await InitializeConfigFolder();
  109. auto configFiles = co_await m_configFolder.GetFilesAsync();
  110. if (configFiles.Size() == 0) {
  111. const auto& defaultConfigDst = co_await CopyDefaultConfig(m_configFolder, L"default.conf");
  112. appData.LocalSettings().Values().Insert(CONFIG_PATH_SETTING_KEY, box_value(defaultConfigDst.Path()));
  113. configFiles = co_await m_configFolder.GetFilesAsync();
  114. }
  115. std::vector<Maple_App::ConfigViewModel> configModels;
  116. configModels.reserve(static_cast<size_t>(configFiles.Size()));
  117. const auto& defaultConfigPath = appData.LocalSettings().Values().TryLookup(CONFIG_PATH_SETTING_KEY).try_as<hstring>();
  118. for (const auto& file : configFiles) {
  119. const auto isDefault = file.Path() == defaultConfigPath;
  120. const auto& instance = configModels.emplace_back(co_await ConfigViewModel::FromFile(file, isDefault));
  121. if (isDefault) {
  122. m_defaultConfig = instance;
  123. }
  124. }
  125. SetValue(m_configItemsProperty, single_threaded_observable_vector<Maple_App::ConfigViewModel>(std::move(configModels)));
  126. ConfigListView().SelectedItem(m_defaultConfig);
  127. }
  128. void MainPage::ConfigSetAsDefaultMenuItem_Click(IInspectable const& sender, RoutedEventArgs const&)
  129. {
  130. auto item = sender.as<FrameworkElement>().DataContext().as<Maple_App::ConfigViewModel>();
  131. if (item == nullptr) {
  132. item = ConfigListView().SelectedItem().as<Maple_App::ConfigViewModel>();
  133. }
  134. SetAsDefault(item);
  135. }
  136. void MainPage::ConfigItem_DoubleTapped(IInspectable const& sender, DoubleTappedRoutedEventArgs const&)
  137. {
  138. const auto& item = sender.as<FrameworkElement>().DataContext().as<Maple_App::ConfigViewModel>();
  139. SetAsDefault(item);
  140. }
  141. void MainPage::SetAsDefault(const Maple_App::ConfigViewModel& item)
  142. {
  143. const auto name = getNormalizedExtentionFromPath(item.Name());
  144. if (name != ".conf" && name != ".json") {
  145. NotifyUser(L"A valid configuration file must end with .conf or .json.");
  146. return;
  147. }
  148. ApplicationData::Current().LocalSettings().Values().Insert(CONFIG_PATH_SETTING_KEY, box_value(item.File().Path()));
  149. m_defaultConfig.IsDefault(false);
  150. item.IsDefault(true);
  151. m_defaultConfig = item;
  152. }
  153. void MainPage::ConfigRenameMenuItem_Click(IInspectable const& sender, RoutedEventArgs const&)
  154. {
  155. auto item = sender.as<FrameworkElement>().DataContext().as<Maple_App::ConfigViewModel>();
  156. if (item == nullptr) {
  157. item = ConfigListView().SelectedItem().as<Maple_App::ConfigViewModel>();
  158. }
  159. RequestRenameItem(item);
  160. }
  161. void MainPage::RequestRenameItem(const Maple_App::ConfigViewModel& item)
  162. {
  163. const auto& name = item.Name();
  164. const auto& renameDialog = RenameDialog();
  165. const auto& renameDialogText = RenameDialogText();
  166. renameDialogText.Text(name);
  167. auto it = std::find(name.rbegin(), name.rend(), '.');
  168. if (it != name.rend()) {
  169. RenameDialogText().Select(0, static_cast<int32_t>(name.rend() - it) - 1);
  170. }
  171. renameDialog.DataContext(item);
  172. renameDialog.ShowAsync();
  173. }
  174. fire_and_forget MainPage::ConfigDeleteMenuItem_Click(IInspectable const& sender, RoutedEventArgs const& e)
  175. {
  176. const auto lifetime = get_strong();
  177. auto item = sender.as<FrameworkElement>().DataContext().as<Maple_App::ConfigViewModel>();
  178. if (item == nullptr) {
  179. item = ConfigListView().SelectedItem().as<Maple_App::ConfigViewModel>();
  180. }
  181. if (item.IsDefault()) {
  182. co_await NotifyUser(L"Default configuration cannot be deleted.");
  183. co_return;
  184. }
  185. uint32_t index;
  186. auto configItems = ConfigItems();
  187. if (!configItems.IndexOf(item, index)) {
  188. co_return;
  189. }
  190. ContentDialog c{};
  191. c.Title(box_value(L"Delete this configuration file?"));
  192. c.Content(box_value(L"This operation cannot be undone."));
  193. c.PrimaryButtonCommand(StandardUICommand{ StandardUICommandKind::Delete });
  194. c.SecondaryButtonCommand(StandardUICommand{ StandardUICommandKind::Close });
  195. const auto& result = co_await c.ShowAsync();
  196. if (result != ContentDialogResult::Primary) {
  197. co_return;
  198. }
  199. if (ConfigListView().SelectedItem() == item) {
  200. ConfigListView().SelectedIndex(ConfigListView().SelectedIndex() == 0 ? 1 : 0);
  201. }
  202. configItems.RemoveAt(index);
  203. co_await item.Delete();
  204. if (configItems.Size() == 0) {
  205. LoadConfigs();
  206. }
  207. }
  208. void MainPage::RenameDialogPrimaryButton_Click(IInspectable const&, ContentDialogButtonClickEventArgs const&)
  209. {
  210. ConfirmRename();
  211. }
  212. void MainPage::RenameDialogText_KeyDown(IInspectable const&, KeyRoutedEventArgs const& e)
  213. {
  214. if (e.Key() == Windows::System::VirtualKey::Enter) {
  215. ConfirmRename();
  216. RenameDialog().Hide();
  217. }
  218. }
  219. fire_and_forget MainPage::ConfirmRename() {
  220. const auto lifetime = get_strong();
  221. const auto& renameDialog = RenameDialog();
  222. const auto& item = renameDialog.DataContext().as<Maple_App::ConfigViewModel>();
  223. if (item == nullptr) {
  224. co_return;
  225. }
  226. co_await item.Rename(RenameDialogText().Text());
  227. if (item == m_defaultConfig) {
  228. ApplicationData::Current().LocalSettings().Values().Insert(CONFIG_PATH_SETTING_KEY, box_value(item.File().Path()));
  229. }
  230. }
  231. fire_and_forget MainPage::ConfigCreateMenuItem_Click(IInspectable const& sender, RoutedEventArgs const&)
  232. {
  233. const auto lifetime = get_strong();
  234. const auto& buttonText = sender.as<MenuFlyoutItem>().Text();
  235. StorageFile newFile{ nullptr };
  236. if (buttonText == L"Conf") {
  237. newFile = co_await CopyDefaultConfig(m_configFolder, L"New Config.conf");
  238. }
  239. else if (buttonText == L"JSON") {
  240. newFile = co_await CopyDefaultJsonConfig(m_configFolder, L"New Config.json");
  241. }
  242. else {
  243. co_return;
  244. }
  245. const auto& item = co_await ConfigViewModel::FromFile(newFile, false);
  246. ConfigItems().Append(item);
  247. RequestRenameItem(item);
  248. }
  249. fire_and_forget MainPage::ConfigImportMenuItem_Click(IInspectable const& sender, RoutedEventArgs const& e)
  250. {
  251. const auto lifetime = get_strong();
  252. bool unsnapped = ((ApplicationView::Value() != ApplicationViewState::Snapped) || ApplicationView::TryUnsnap());
  253. if (!unsnapped)
  254. {
  255. co_await NotifyUser(L"Cannot unsnap the app.");
  256. co_return;
  257. }
  258. ImportFilePicker().FileTypeFilter().ReplaceAll({ L".conf", L".json", L".mmdb", L".dat", L".cer", L".crt" });
  259. const auto& files = co_await ImportFilePicker().PickMultipleFilesAsync();
  260. co_await ImportFiles(files);
  261. }
  262. fire_and_forget MainPage::ConfigDuplicateMenuItem_Click(IInspectable const& sender, RoutedEventArgs const&)
  263. {
  264. const auto lifetime = get_strong();
  265. auto item = sender.as<FrameworkElement>().DataContext().as<Maple_App::ConfigViewModel>();
  266. if (item == nullptr) {
  267. item = ConfigListView().SelectedItem().as<Maple_App::ConfigViewModel>();
  268. }
  269. const auto& file = item.File();
  270. const auto& parent = co_await file.GetParentAsync();
  271. const auto& newFile = co_await file.CopyAsync(parent, file.Name(), NameCollisionOption::GenerateUniqueName);
  272. ConfigItems().Append(co_await ConfigViewModel::FromFile(newFile, false));
  273. }
  274. void MainPage::MainPivot_PivotItemLoaded(Pivot const&, PivotItemEventArgs const& args)
  275. {
  276. if (args.Item().Header().as<hstring>() == L"Setting") {
  277. const auto& netifs = Netif::EnumerateInterfaces();
  278. std::vector<IInspectable> boxed_netifs;
  279. boxed_netifs.reserve(netifs.size());
  280. std::transform(netifs.begin(), netifs.end(), std::back_inserter(boxed_netifs), [](const auto& netif) -> auto {
  281. return netif;
  282. });
  283. NetifCombobox().ItemsSource(single_threaded_vector(std::move(boxed_netifs)));
  284. const auto& currentNetif = ApplicationData::Current().LocalSettings().Values().TryLookup(NETIF_SETTING_KEY).try_as<hstring>();
  285. if (currentNetif.has_value()) {
  286. NetifCombobox().SelectedValue(box_value(currentNetif.value()));
  287. }
  288. else {
  289. const auto it = std::find_if(netifs.begin(), netifs.end(), [](const auto& netif) -> bool {
  290. return netif.Desc().size() > 0 && netif.Desc()[0] == L'★';
  291. });
  292. if (it != netifs.end()) {
  293. NetifCombobox().SelectedItem(*it);
  294. }
  295. }
  296. }
  297. }
  298. void MainPage::NetifCombobox_SelectionChanged(IInspectable const&, SelectionChangedEventArgs const& e)
  299. {
  300. const auto it = e.AddedItems().First();
  301. if (!it.HasCurrent() || it.Current().try_as<Maple_App::Netif>() == nullptr) {
  302. return;
  303. }
  304. const auto& netif = it.Current().as<Maple_App::Netif>();
  305. ApplicationData::Current().LocalSettings().Values().Insert(NETIF_SETTING_KEY, box_value(netif.Addr()));
  306. }
  307. void MainPage::ConfigListView_SelectionChanged(IInspectable const&, SelectionChangedEventArgs const& e)
  308. {
  309. const auto& item = e.AddedItems().First().Current();
  310. auto targetPage = xaml_typename<MonacoEditPage>();
  311. const auto& config = item.try_as<Maple_App::ConfigViewModel>();
  312. if (config != nullptr) {
  313. const auto ext = getNormalizedExtentionFromPath(config.Name());
  314. if (ext == ".mmdb") {
  315. targetPage = xaml_typename<MmdbPage>();
  316. }
  317. else if (ext == ".dat") {
  318. targetPage = xaml_typename<DatPage>();
  319. }
  320. else if (ext == ".cer" || ext == ".crt") {
  321. targetPage = xaml_typename<CertPage>();
  322. }
  323. }
  324. if (targetPage.Name != xaml_typename<MonacoEditPage>().Name)
  325. {
  326. MainContentFrame().BackStack().Clear();
  327. }
  328. MainContentFrame().Navigate(targetPage, item);
  329. }
  330. void MainPage::ConfigListView_DragItemsStarting(IInspectable const&, DragItemsStartingEventArgs const& e)
  331. {
  332. std::vector<IStorageItem> files;
  333. files.reserve(static_cast<size_t>(e.Items().Size()));
  334. for (const auto& obj : e.Items()) {
  335. const auto& item = obj.try_as<Maple_App::ConfigViewModel>();
  336. if (item == nullptr) {
  337. continue;
  338. }
  339. files.push_back(item.File());
  340. }
  341. const auto& data = e.Data();
  342. data.SetStorageItems(files);
  343. data.RequestedOperation(DataPackageOperation::Copy);
  344. }
  345. void MainPage::ConfigListView_DragOver(IInspectable const&, DragEventArgs const& e)
  346. {
  347. if (static_cast<uint32_t>(e.AllowedOperations() & DataPackageOperation::Copy) == 0
  348. || !e.DataView().Contains(StandardDataFormats::StorageItems())) {
  349. e.AcceptedOperation(DataPackageOperation::None);
  350. return;
  351. }
  352. e.AcceptedOperation(DataPackageOperation::Copy);
  353. }
  354. fire_and_forget MainPage::ConfigListView_Drop(IInspectable const&, DragEventArgs const& e)
  355. {
  356. const auto lifetime = get_strong();
  357. const auto& dataView = e.DataView();
  358. if (static_cast<uint32_t>(e.AllowedOperations() & DataPackageOperation::Copy) == 0
  359. || !dataView.Contains(StandardDataFormats::StorageItems())) {
  360. co_return;
  361. }
  362. const auto& items = co_await dataView.GetStorageItemsAsync();
  363. co_await ImportFiles(items);
  364. }
  365. void MainPage::WindowWidth_CurrentStateChanged(IInspectable const&, VisualStateChangedEventArgs const& e)
  366. {
  367. const auto& state = e.NewState();
  368. NavigationManager.AppViewBackButtonVisibility(state == nullptr
  369. ? AppViewBackButtonVisibility::Visible
  370. : AppViewBackButtonVisibility::Collapsed);
  371. }
  372. void MainPage::MainSplitView_PaneClosing(SplitView const&, SplitViewPaneClosingEventArgs const&)
  373. {
  374. NavigationManager.AppViewBackButtonVisibility(AppViewBackButtonVisibility::Visible);
  375. }
  376. fire_and_forget MainPage::GenerateProfileButton_Click(IInspectable const& sender, RoutedEventArgs const& e)
  377. {
  378. const auto lifetime = get_strong();
  379. const auto& profile = VpnPlugInProfile{};
  380. profile.AlwaysOn(false);
  381. profile.ProfileName(L"Maple");
  382. profile.RequireVpnClientAppUI(true);
  383. profile.VpnPluginPackageFamilyName(Windows::ApplicationModel::Package::Current().Id().FamilyName());
  384. profile.RememberCredentials(false);
  385. profile.ServerUris().Append(Uri{ L"https://github.com/YtFlow/Maple" });
  386. const auto& result = co_await VpnMgmtAgent.AddProfileFromObjectAsync(profile);
  387. if (result == VpnManagementErrorStatus::Ok) {
  388. co_await NotifyUser(L"Profile generated.");
  389. }
  390. else {
  391. co_await NotifyUser(L"Failed to generate a profile (" + to_hstring(static_cast<int32_t>(result)) + L").");
  392. }
  393. }
  394. fire_and_forget MainPage::ConnectionToggleSwitch_Toggled(IInspectable const&, RoutedEventArgs const&)
  395. {
  396. const auto lifetime{ get_strong() };
  397. if (!ApplicationData::Current().LocalSettings().Values().HasKey(NETIF_SETTING_KEY)) {
  398. MainPivot().SelectedIndex(1);
  399. co_await 400ms;
  400. co_await resume_foreground(Dispatcher());
  401. NetifCombobox().IsDropDownOpen(true);
  402. co_return;
  403. }
  404. const auto connect = ConnectionToggleSwitch().IsOn();
  405. ConnectionToggleSwitch().IsEnabled(false);
  406. VpnManagementErrorStatus status = VpnManagementErrorStatus::Ok;
  407. if (connect) {
  408. status = co_await VpnMgmtAgent.ConnectProfileAsync(m_vpnProfile);
  409. }
  410. else {
  411. status = co_await VpnMgmtAgent.DisconnectProfileAsync(m_vpnProfile);
  412. }
  413. if (status == VpnManagementErrorStatus::Ok)
  414. {
  415. ConnectionToggleSwitch().IsEnabled(true);
  416. }
  417. else {
  418. NotifyUser(L"Could not perform the requested operation. Please try again from system VPN settings for detailed error messages.");
  419. }
  420. }
  421. fire_and_forget MainPage::StartConnectionCheck()
  422. {
  423. const auto lifetime{ get_strong() };
  424. IVectorView<IVpnProfile> profiles{ nullptr };
  425. auto event_token{ ConnectionToggleSwitch().Toggled({ this, &MainPage::ConnectionToggleSwitch_Toggled }) };
  426. while (true) {
  427. if (m_vpnProfile == nullptr) {
  428. profiles = co_await VpnMgmtAgent.GetProfilesAsync();
  429. for (auto const p : profiles) {
  430. if (p.ProfileName() == L"Maple" || p.ProfileName() == L"maple") {
  431. m_vpnProfile = p.try_as<VpnPlugInProfile>();
  432. break;
  433. }
  434. }
  435. }
  436. if (m_vpnProfile == nullptr) {
  437. ConnectionToggleSwitch().IsEnabled(false);
  438. }
  439. else {
  440. ToolTipService::SetToolTip(ConnectionToggleSwitchContainer(), nullptr);
  441. auto status = VpnManagementConnectionStatus::Disconnected;
  442. try {
  443. status = m_vpnProfile.ConnectionStatus();
  444. }
  445. catch (...) {}
  446. ConnectionToggleSwitch().IsEnabled(status == VpnManagementConnectionStatus::Connected
  447. || status == VpnManagementConnectionStatus::Disconnected);
  448. ConnectionToggleSwitch().Toggled(event_token);
  449. ConnectionToggleSwitch().IsOn(status == VpnManagementConnectionStatus::Connected
  450. || status == VpnManagementConnectionStatus::Connecting);
  451. event_token = ConnectionToggleSwitch().Toggled({ this, &MainPage::ConnectionToggleSwitch_Toggled });
  452. }
  453. co_await 1s;
  454. co_await resume_foreground(Dispatcher());
  455. }
  456. }
  457. }