Ver Fonte

Handle exceptions

bdbai há 2 anos atrás
pai
commit
0fa939503c

+ 12 - 9
Maple.App/App.cpp

@@ -6,6 +6,7 @@
 #include "MainPage.h"
 #include "EditPage.h"
 #include "MonacoEditPage.h"
+#include "UI.h"
 
 using namespace winrt;
 using namespace Windows::ApplicationModel;
@@ -35,16 +36,18 @@ App::App()
     InitializeComponent();
     Suspending({ this, &App::OnSuspending });
 
-#if defined _DEBUG && !defined DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION
     UnhandledException([this](IInspectable const&, UnhandledExceptionEventArgs const& e)
         {
-            if (IsDebuggerPresent())
-            {
-                auto errorMessage = e.Message();
-                __debugbreak();
-            }
-        });
+            auto errorMessage = e.Message();
+#if defined _DEBUG && !defined DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION
+    if (IsDebuggerPresent())
+    {
+        __debugbreak();
+    }
 #endif
+    UI::NotifyUser(std::move(errorMessage), L"Unexpected error");
+    e.Handled(true);
+        });
 }
 
 /// <summary>
@@ -120,8 +123,8 @@ fire_and_forget App::OnSuspending([[maybe_unused]] IInspectable const& sender, [
 {
     using namespace std::literals::chrono_literals;
     // Save application state and stop any background activity
-    const auto& saveModifiedContent = EditPage::SaveModifiedContent;
-    const auto& saveMonacoModifiedContent = MonacoEditPage::SaveModifiedContent;
+    const auto saveModifiedContent = std::move(EditPage::SaveModifiedContent);
+    const auto saveMonacoModifiedContent = std::move(MonacoEditPage::SaveModifiedContent);
     if (saveModifiedContent == nullptr && saveMonacoModifiedContent == nullptr) {
         co_return;
     }

+ 79 - 59
Maple.App/EditPage.cpp

@@ -4,6 +4,8 @@
 #include "EditPage.g.cpp"
 #endif
 
+#include "UI.h"
+
 using namespace std::literals::chrono_literals;
 using namespace winrt;
 using namespace winrt::Windows::Storage::Streams;
@@ -17,24 +19,30 @@ namespace winrt::Maple_App::implementation
     }
 
     fire_and_forget EditPage::OnNavigatedTo(NavigationEventArgs const& e) {
-        const auto lifetime = get_strong();
-        const auto& param = e.Parameter().as<Maple_App::ConfigViewModel>();
-
-        m_file = param.File();
         try {
-            const auto& text = co_await FileIO::ReadTextAsync(param.File(), UnicodeEncoding::Utf8);
-            EditBox().Document().SetText(TextSetOptions::None, text);
+            const auto lifetime = get_strong();
+            const auto& param = e.Parameter().as<Maple_App::ConfigViewModel>();
+
+            m_file = param.File();
+            try {
+                const auto& text = co_await FileIO::ReadTextAsync(param.File(), UnicodeEncoding::Utf8);
+                EditBox().Document().SetText(TextSetOptions::None, text);
+            }
+            catch (const winrt::hresult_error&) {
+                EditBox().Document().SetText(TextSetOptions::None, L"Invalid configuration file");
+            }
+            const auto weakThis = lifetime->get_weak();
+            m_saveModifiedContent = [weakThis]() -> IAsyncAction {
+                if (const auto self{ weakThis.get() }) {
+                    return self->SaveDocument();
+                }
+                return {};
+            };
         }
-        catch (const winrt::hresult_error&) {
-            EditBox().Document().SetText(TextSetOptions::None, L"Invalid configuration file");
+        catch (...)
+        {
+            UI::NotifyException(L"Failed to load file");
         }
-        const auto weakThis = lifetime->get_weak();
-        m_saveModifiedContent = [weakThis]() -> IAsyncAction {
-            if (const auto self{ weakThis.get() }) {
-                return self->SaveDocument();
-            }
-            return {};
-        };
     }
     void EditPage::OnNavigatingFrom(NavigatingCancelEventArgs const&) {
         if (m_file == nullptr || !m_file.IsAvailable()) {
@@ -54,61 +62,73 @@ namespace winrt::Maple_App::implementation
         }
     }
     fire_and_forget EditPage::SaveButton_Click(IInspectable const& sender, RoutedEventArgs const&) {
-        const auto lifetime = get_strong();
-        const auto& placementTarget = sender.try_as<FrameworkElement>();
-        const auto currentValidateRequest = ++validateRequest;
-        ValidConfigFlyout().Hide();
-        InvalidConfigFlyout().Hide();
-        co_await SaveDocument();
-        if (validateRequest != currentValidateRequest) {
-            co_return;
-        }
+        try {
+            const auto lifetime = get_strong();
+            const auto& placementTarget = sender.try_as<FrameworkElement>();
+            const auto currentValidateRequest = ++validateRequest;
+            ValidConfigFlyout().Hide();
+            InvalidConfigFlyout().Hide();
+            co_await SaveDocument();
+            if (validateRequest != currentValidateRequest) {
+                co_return;
+            }
 
-        // Validate
-        const auto& path = winrt::to_string(m_file.Path());
-        co_await winrt::resume_background();
-        const auto result = leaf_test_config(path.data());
-        co_await winrt::resume_foreground(Dispatcher());
-        if (validateRequest != currentValidateRequest) {
-            co_return;
+            // Validate
+            const auto& path = winrt::to_string(m_file.Path());
+            co_await winrt::resume_background();
+            const auto result = leaf_test_config(path.data());
+            co_await winrt::resume_foreground(Dispatcher());
+            if (validateRequest != currentValidateRequest) {
+                co_return;
+            }
+            switch (result)
+            {
+            case LEAF_ERR_OK:
+                ValidConfigFlyout().ShowAt(placementTarget);
+                break;
+            case LEAF_ERR_CONFIG:
+                InvalidConfigFlyout().ShowAt(placementTarget);
+                break;
+            default:
+                // TODO: handle errors
+                break;
+            }
+            co_await winrt::resume_after(2s);
+            co_await winrt::resume_foreground(Dispatcher());
+            if (validateRequest != currentValidateRequest) {
+                co_return;
+            }
+            ValidConfigFlyout().Hide();
+            InvalidConfigFlyout().Hide();
         }
-        switch (result)
+        catch (...)
         {
-        case LEAF_ERR_OK:
-            ValidConfigFlyout().ShowAt(placementTarget);
-            break;
-        case LEAF_ERR_CONFIG:
-            InvalidConfigFlyout().ShowAt(placementTarget);
-            break;
-        default:
-            // TODO: handle errors
-            break;
-        }
-        co_await winrt::resume_after(2s);
-        co_await winrt::resume_foreground(Dispatcher());
-        if (validateRequest != currentValidateRequest) {
-            co_return;
+            UI::NotifyException(L"Saving file");
         }
-        ValidConfigFlyout().Hide();
-        InvalidConfigFlyout().Hide();
     }
     void EditPage::HelpButton_Click(IInspectable const&, RoutedEventArgs const&) {
         const auto _ = winrt::Windows::System::Launcher::LaunchUriAsync(Uri{ L"https://github.com/eycorsican/leaf/blob/master/README.zh.md" });
     }
     IAsyncAction EditPage::SaveDocument()
     {
-        const auto lifetime = get_strong();
-        if (!SaveButton().IsEnabled()) {
-            co_return;
+        try {
+            const auto lifetime = get_strong();
+            if (!SaveButton().IsEnabled()) {
+                co_return;
+            }
+            SaveModifiedContent = nullptr;
+            SaveButton().IsEnabled(false);
+            hstring content{};
+            EditBox().Document().GetText(TextGetOptions::NoHidden | TextGetOptions::UseCrlf, content);
+            const auto data = to_string(content);
+            co_return co_await FileIO::WriteBytesAsync(
+                m_file,
+                std::vector<uint8_t>(data.begin(), data.end()));
+        }
+        catch (...)
+        {
+            UI::NotifyException(L"Saving document");
         }
-        SaveModifiedContent = nullptr;
-        SaveButton().IsEnabled(false);
-        hstring content{};
-        EditBox().Document().GetText(TextGetOptions::NoHidden | TextGetOptions::UseCrlf, content);
-        const auto data = to_string(content);
-        co_return co_await FileIO::WriteBytesAsync(
-            m_file,
-            std::vector<uint8_t>(data.begin(), data.end()));
     }
 }
 

+ 312 - 209
Maple.App/MainPage.cpp

@@ -1,4 +1,4 @@
-#include "pch.h"
+#include "pch.h"
 #include "MainPage.h"
 #include "MainPage.g.cpp"
 #include <filesystem>
@@ -6,6 +6,7 @@
 #include <winrt/Windows.UI.ViewManagement.h>
 #include <winrt/Windows.UI.Xaml.Media.h>
 #include "Model\Netif.h"
+#include "UI.h"
 
 namespace winrt::Maple_App::implementation
 {
@@ -146,11 +147,17 @@ namespace winrt::Maple_App::implementation
 
     void MainPage::ConfigSetAsDefaultMenuItem_Click(IInspectable const& sender, RoutedEventArgs const&)
     {
-        auto item = sender.as<FrameworkElement>().DataContext().as<Maple_App::ConfigViewModel>();
-        if (item == nullptr) {
-            item = ConfigListView().SelectedItem().as<Maple_App::ConfigViewModel>();
+        try {
+            auto item = sender.as<FrameworkElement>().DataContext().as<Maple_App::ConfigViewModel>();
+            if (item == nullptr) {
+                item = ConfigListView().SelectedItem().as<Maple_App::ConfigViewModel>();
+            }
+            SetAsDefault(item);
+        }
+        catch (...)
+        {
+            UI::NotifyException(L"Setting default");
         }
-        SetAsDefault(item);
     }
 
     void MainPage::ConfigItem_DoubleTapped(IInspectable const& sender, DoubleTappedRoutedEventArgs const&)
@@ -161,24 +168,36 @@ namespace winrt::Maple_App::implementation
 
     void MainPage::SetAsDefault(const Maple_App::ConfigViewModel& item)
     {
-        const auto name = getNormalizedExtentionFromPath(item.Name());
-        if (name != ".conf" && name != ".json") {
-            NotifyUser(L"A valid configuration file must end with .conf or .json.");
-            return;
+        try {
+            const auto name = getNormalizedExtentionFromPath(item.Name());
+            if (name != ".conf" && name != ".json") {
+                NotifyUser(L"A valid configuration file must end with .conf or .json.");
+                return;
+            }
+            ApplicationData::Current().LocalSettings().Values().Insert(CONFIG_PATH_SETTING_KEY, box_value(item.File().Path()));
+            m_defaultConfig.IsDefault(false);
+            item.IsDefault(true);
+            m_defaultConfig = item;
+        }
+        catch (...)
+        {
+            UI::NotifyException(L"Setting default");
         }
-        ApplicationData::Current().LocalSettings().Values().Insert(CONFIG_PATH_SETTING_KEY, box_value(item.File().Path()));
-        m_defaultConfig.IsDefault(false);
-        item.IsDefault(true);
-        m_defaultConfig = item;
     }
 
     void MainPage::ConfigRenameMenuItem_Click(IInspectable const& sender, RoutedEventArgs const&)
     {
-        auto item = sender.as<FrameworkElement>().DataContext().as<Maple_App::ConfigViewModel>();
-        if (item == nullptr) {
-            item = ConfigListView().SelectedItem().as<Maple_App::ConfigViewModel>();
+        try {
+            auto item = sender.as<FrameworkElement>().DataContext().as<Maple_App::ConfigViewModel>();
+            if (item == nullptr) {
+                item = ConfigListView().SelectedItem().as<Maple_App::ConfigViewModel>();
+            }
+            RequestRenameItem(item);
+        }
+        catch (...)
+        {
+            UI::NotifyException(L"Requesting rename");
         }
-        RequestRenameItem(item);
     }
 
     void MainPage::RequestRenameItem(const Maple_App::ConfigViewModel& item)
@@ -197,37 +216,43 @@ namespace winrt::Maple_App::implementation
 
     fire_and_forget MainPage::ConfigDeleteMenuItem_Click(IInspectable const& sender, RoutedEventArgs const& e)
     {
-        const auto lifetime = get_strong();
-        auto item = sender.as<FrameworkElement>().DataContext().as<Maple_App::ConfigViewModel>();
-        if (item == nullptr) {
-            item = ConfigListView().SelectedItem().as<Maple_App::ConfigViewModel>();
-        }
-        if (item.IsDefault()) {
-            co_await NotifyUser(L"Default configuration cannot be deleted.");
-            co_return;
-        }
-        uint32_t index;
-        auto configItems = ConfigItems();
-        if (!configItems.IndexOf(item, index)) {
-            co_return;
-        }
-        ContentDialog c{};
-        c.Title(box_value(L"Delete this configuration file?"));
-        c.Content(box_value(L"This operation cannot be undone."));
-        c.PrimaryButtonCommand(StandardUICommand{ StandardUICommandKind::Delete });
-        c.SecondaryButtonCommand(StandardUICommand{ StandardUICommandKind::Close });
-        const auto& result = co_await c.ShowAsync();
-        if (result != ContentDialogResult::Primary) {
-            co_return;
-        }
+        try {
+            const auto lifetime = get_strong();
+            auto item = sender.as<FrameworkElement>().DataContext().as<Maple_App::ConfigViewModel>();
+            if (item == nullptr) {
+                item = ConfigListView().SelectedItem().as<Maple_App::ConfigViewModel>();
+            }
+            if (item.IsDefault()) {
+                co_await NotifyUser(L"Default configuration cannot be deleted.");
+                co_return;
+            }
+            uint32_t index;
+            auto configItems = ConfigItems();
+            if (!configItems.IndexOf(item, index)) {
+                co_return;
+            }
+            ContentDialog c{};
+            c.Title(box_value(L"Delete this configuration file?"));
+            c.Content(box_value(L"This operation cannot be undone."));
+            c.PrimaryButtonCommand(StandardUICommand{ StandardUICommandKind::Delete });
+            c.SecondaryButtonCommand(StandardUICommand{ StandardUICommandKind::Close });
+            const auto& result = co_await c.ShowAsync();
+            if (result != ContentDialogResult::Primary) {
+                co_return;
+            }
 
-        if (ConfigListView().SelectedItem() == item) {
-            ConfigListView().SelectedIndex(ConfigListView().SelectedIndex() == 0 ? 1 : 0);
+            if (ConfigListView().SelectedItem() == item) {
+                ConfigListView().SelectedIndex(ConfigListView().SelectedIndex() == 0 ? 1 : 0);
+            }
+            configItems.RemoveAt(index);
+            co_await item.Delete();
+            if (configItems.Size() == 0) {
+                LoadConfigs();
+            }
         }
-        configItems.RemoveAt(index);
-        co_await item.Delete();
-        if (configItems.Size() == 0) {
-            LoadConfigs();
+        catch (...)
+        {
+            UI::NotifyException(L"Deleting file");
         }
     }
 
@@ -245,160 +270,220 @@ namespace winrt::Maple_App::implementation
     }
 
     fire_and_forget MainPage::ConfirmRename() {
-        const auto lifetime = get_strong();
-        const auto& renameDialog = RenameDialog();
-        const auto& item = renameDialog.DataContext().as<Maple_App::ConfigViewModel>();
-        if (item == nullptr) {
-            co_return;
+        try {
+            const auto lifetime = get_strong();
+            const auto& renameDialog = RenameDialog();
+            const auto& item = renameDialog.DataContext().as<Maple_App::ConfigViewModel>();
+            if (item == nullptr) {
+                co_return;
+            }
+            co_await item.Rename(RenameDialogText().Text());
+            if (item == m_defaultConfig) {
+                ApplicationData::Current().LocalSettings().Values().Insert(CONFIG_PATH_SETTING_KEY, box_value(item.File().Path()));
+            }
         }
-        co_await item.Rename(RenameDialogText().Text());
-        if (item == m_defaultConfig) {
-            ApplicationData::Current().LocalSettings().Values().Insert(CONFIG_PATH_SETTING_KEY, box_value(item.File().Path()));
+        catch (...)
+        {
+            UI::NotifyException(L"Renaming");
         }
     }
 
     fire_and_forget MainPage::ConfigCreateMenuItem_Click(IInspectable const& sender, RoutedEventArgs const&)
     {
-        const auto lifetime = get_strong();
-        const auto& buttonText = sender.as<MenuFlyoutItem>().Text();
-        StorageFile newFile{ nullptr };
-        if (buttonText == L"Conf") {
-            newFile = co_await CopyDefaultConfig(m_configFolder, L"New Config.conf");
-        }
-        else if (buttonText == L"JSON") {
-            newFile = co_await CopyDefaultJsonConfig(m_configFolder, L"New Config.json");
+        try {
+            const auto lifetime = get_strong();
+            const auto& buttonText = sender.as<MenuFlyoutItem>().Text();
+            StorageFile newFile{ nullptr };
+            if (buttonText == L"Conf") {
+                newFile = co_await CopyDefaultConfig(m_configFolder, L"New Config.conf");
+            }
+            else if (buttonText == L"JSON") {
+                newFile = co_await CopyDefaultJsonConfig(m_configFolder, L"New Config.json");
+            }
+            else {
+                co_return;
+            }
+            const auto& item = co_await ConfigViewModel::FromFile(newFile, false);
+            ConfigItems().Append(item);
+            RequestRenameItem(item);
         }
-        else {
-            co_return;
+        catch (...)
+        {
+            UI::NotifyException(L"Creating file");
         }
-        const auto& item = co_await ConfigViewModel::FromFile(newFile, false);
-        ConfigItems().Append(item);
-        RequestRenameItem(item);
     }
 
     fire_and_forget MainPage::ConfigImportMenuItem_Click(IInspectable const& sender, RoutedEventArgs const& e)
     {
-        const auto lifetime = get_strong();
-        bool unsnapped = ((ApplicationView::Value() != ApplicationViewState::Snapped) || ApplicationView::TryUnsnap());
-        if (!unsnapped)
+        try {
+            const auto lifetime = get_strong();
+            bool unsnapped = ((ApplicationView::Value() != ApplicationViewState::Snapped) || ApplicationView::TryUnsnap());
+            if (!unsnapped)
+            {
+                co_await NotifyUser(L"Cannot unsnap the app.");
+                co_return;
+            }
+            ImportFilePicker().FileTypeFilter().ReplaceAll({ L".conf", L".json", L".mmdb", L".dat", L".cer", L".crt" });
+            const auto& files = co_await ImportFilePicker().PickMultipleFilesAsync();
+            co_await ImportFiles(files);
+        }
+        catch (...)
         {
-            co_await NotifyUser(L"Cannot unsnap the app.");
-            co_return;
+            UI::NotifyException(L"Importing files");
         }
-        ImportFilePicker().FileTypeFilter().ReplaceAll({ L".conf", L".json", L".mmdb", L".dat", L".cer", L".crt" });
-        const auto& files = co_await ImportFilePicker().PickMultipleFilesAsync();
-        co_await ImportFiles(files);
     }
 
     fire_and_forget MainPage::ConfigDuplicateMenuItem_Click(IInspectable const& sender, RoutedEventArgs const&)
     {
-        const auto lifetime = get_strong();
-        auto item = sender.as<FrameworkElement>().DataContext().as<Maple_App::ConfigViewModel>();
-        if (item == nullptr) {
-            item = ConfigListView().SelectedItem().as<Maple_App::ConfigViewModel>();
+        try {
+            const auto lifetime = get_strong();
+            auto item = sender.as<FrameworkElement>().DataContext().as<Maple_App::ConfigViewModel>();
+            if (item == nullptr) {
+                item = ConfigListView().SelectedItem().as<Maple_App::ConfigViewModel>();
+            }
+            const auto& file = item.File();
+            const auto& parent = co_await file.GetParentAsync();
+            const auto& newFile = co_await file.CopyAsync(parent, file.Name(), NameCollisionOption::GenerateUniqueName);
+            ConfigItems().Append(co_await ConfigViewModel::FromFile(newFile, false));
+        }
+        catch (...)
+        {
+            UI::NotifyException(L"Duplicating");
         }
-        const auto& file = item.File();
-        const auto& parent = co_await file.GetParentAsync();
-        const auto& newFile = co_await file.CopyAsync(parent, file.Name(), NameCollisionOption::GenerateUniqueName);
-        ConfigItems().Append(co_await ConfigViewModel::FromFile(newFile, false));
     }
 
     void MainPage::MainPivot_PivotItemLoaded(Pivot const&, PivotItemEventArgs const& args)
     {
-        if (args.Item().Header().as<hstring>() == L"Setting") {
-            const auto& netifs = Netif::EnumerateInterfaces();
-            std::vector<IInspectable> boxed_netifs;
-            boxed_netifs.reserve(netifs.size());
-            std::transform(netifs.begin(), netifs.end(), std::back_inserter(boxed_netifs), [](const auto& netif) -> auto {
-                return netif;
-                });
-            NetifCombobox().ItemsSource(single_threaded_vector(std::move(boxed_netifs)));
-
-            const auto& currentNetif = ApplicationData::Current().LocalSettings().Values().TryLookup(NETIF_SETTING_KEY).try_as<hstring>();
-            if (currentNetif.has_value()) {
-                NetifCombobox().SelectedValue(box_value(currentNetif.value()));
-            }
-            else {
-                const auto it = std::find_if(netifs.begin(), netifs.end(), [](const auto& netif) -> bool {
-                    return netif.Desc().size() > 0 && netif.Desc()[0] == L'★';
+        try {
+            if (args.Item().Header().as<hstring>() == L"Setting") {
+                const auto& netifs = Netif::EnumerateInterfaces();
+                std::vector<IInspectable> boxed_netifs;
+                boxed_netifs.reserve(netifs.size());
+                std::transform(netifs.begin(), netifs.end(), std::back_inserter(boxed_netifs), [](const auto& netif) -> auto {
+                    return netif;
                     });
-                if (it != netifs.end()) {
-                    NetifCombobox().SelectedItem(*it);
+                NetifCombobox().ItemsSource(single_threaded_vector(std::move(boxed_netifs)));
+
+                const auto& currentNetif = ApplicationData::Current().LocalSettings().Values().TryLookup(NETIF_SETTING_KEY).try_as<hstring>();
+                if (currentNetif.has_value()) {
+                    NetifCombobox().SelectedValue(box_value(currentNetif.value()));
+                }
+                else {
+                    const auto it = std::find_if(netifs.begin(), netifs.end(), [](const auto& netif) -> bool {
+                        return netif.Desc().size() > 0 && netif.Desc()[0] == L'★';
+                        });
+                    if (it != netifs.end()) {
+                        NetifCombobox().SelectedItem(*it);
+                    }
                 }
             }
         }
+        catch (...)
+        {
+            UI::NotifyException(L"Loading settings");
+        }
     }
     void MainPage::NetifCombobox_SelectionChanged(IInspectable const&, SelectionChangedEventArgs const& e)
     {
-        const auto it = e.AddedItems().First();
-        if (!it.HasCurrent() || it.Current().try_as<Maple_App::Netif>() == nullptr) {
-            return;
-        }
+        try {
+            const auto it = e.AddedItems().First();
+            if (!it.HasCurrent() || it.Current().try_as<Maple_App::Netif>() == nullptr) {
+                return;
+            }
 
-        const auto& netif = it.Current().as<Maple_App::Netif>();
-        ApplicationData::Current().LocalSettings().Values().Insert(NETIF_SETTING_KEY, box_value(netif.Addr()));
+            const auto& netif = it.Current().as<Maple_App::Netif>();
+            ApplicationData::Current().LocalSettings().Values().Insert(NETIF_SETTING_KEY, box_value(netif.Addr()));
+        }
+        catch (...)
+        {
+            UI::NotifyException(L"Setting interface");
+        }
     }
 
     void MainPage::ConfigListView_SelectionChanged(IInspectable const&, SelectionChangedEventArgs const& e)
     {
-        const auto& item = e.AddedItems().First().Current();
-        auto targetPage = xaml_typename<MonacoEditPage>();
-        const auto& config = item.try_as<Maple_App::ConfigViewModel>();
-        if (config != nullptr) {
-            const auto ext = getNormalizedExtentionFromPath(config.Name());
-            if (ext == ".mmdb") {
-                targetPage = xaml_typename<MmdbPage>();
-            }
-            else if (ext == ".dat") {
-                targetPage = xaml_typename<DatPage>();
+        try {
+            const auto& item = e.AddedItems().First().Current();
+            auto targetPage = xaml_typename<MonacoEditPage>();
+            const auto& config = item.try_as<Maple_App::ConfigViewModel>();
+            if (config != nullptr) {
+                const auto ext = getNormalizedExtentionFromPath(config.Name());
+                if (ext == ".mmdb") {
+                    targetPage = xaml_typename<MmdbPage>();
+                }
+                else if (ext == ".dat") {
+                    targetPage = xaml_typename<DatPage>();
+                }
+                else if (ext == ".cer" || ext == ".crt") {
+                    targetPage = xaml_typename<CertPage>();
+                }
             }
-            else if (ext == ".cer" || ext == ".crt") {
-                targetPage = xaml_typename<CertPage>();
+            if (targetPage.Name != xaml_typename<MonacoEditPage>().Name)
+            {
+                MainContentFrame().BackStack().Clear();
             }
+            MainContentFrame().Navigate(targetPage, item);
         }
-        if (targetPage.Name != xaml_typename<MonacoEditPage>().Name)
+        catch (...)
         {
-            MainContentFrame().BackStack().Clear();
+            UI::NotifyException(L"Opening file");
         }
-        MainContentFrame().Navigate(targetPage, item);
     }
 
     void MainPage::ConfigListView_DragItemsStarting(IInspectable const&, DragItemsStartingEventArgs const& e)
     {
-        std::vector<IStorageItem> files;
-        files.reserve(static_cast<size_t>(e.Items().Size()));
-        for (const auto& obj : e.Items()) {
-            const auto& item = obj.try_as<Maple_App::ConfigViewModel>();
-            if (item == nullptr) {
-                continue;
+        try {
+            std::vector<IStorageItem> files;
+            files.reserve(static_cast<size_t>(e.Items().Size()));
+            for (const auto& obj : e.Items()) {
+                const auto& item = obj.try_as<Maple_App::ConfigViewModel>();
+                if (item == nullptr) {
+                    continue;
+                }
+                files.push_back(item.File());
             }
-            files.push_back(item.File());
+            const auto& data = e.Data();
+            data.SetStorageItems(files);
+            data.RequestedOperation(DataPackageOperation::Copy);
+        }
+        catch (...)
+        {
+            UI::NotifyException(L"Preparing drag items");
         }
-        const auto& data = e.Data();
-        data.SetStorageItems(files);
-        data.RequestedOperation(DataPackageOperation::Copy);
     }
 
     void MainPage::ConfigListView_DragOver(IInspectable const&, DragEventArgs const& e)
     {
-        if (static_cast<uint32_t>(e.AllowedOperations() & DataPackageOperation::Copy) == 0
-            || !e.DataView().Contains(StandardDataFormats::StorageItems())) {
-            e.AcceptedOperation(DataPackageOperation::None);
-            return;
+        try {
+            if (static_cast<uint32_t>(e.AllowedOperations() & DataPackageOperation::Copy) == 0
+                || !e.DataView().Contains(StandardDataFormats::StorageItems())) {
+                e.AcceptedOperation(DataPackageOperation::None);
+                return;
+            }
+            e.AcceptedOperation(DataPackageOperation::Copy);
+        }
+        catch (...)
+        {
+            UI::NotifyException(L"Dragging");
         }
-        e.AcceptedOperation(DataPackageOperation::Copy);
     }
     fire_and_forget MainPage::ConfigListView_Drop(IInspectable const&, DragEventArgs const& e)
     {
-        const auto lifetime = get_strong();
-        const auto& dataView = e.DataView();
-        if (static_cast<uint32_t>(e.AllowedOperations() & DataPackageOperation::Copy) == 0
-            || !dataView.Contains(StandardDataFormats::StorageItems())) {
-            co_return;
-        }
+        try {
+            const auto lifetime = get_strong();
+            const auto& dataView = e.DataView();
+            if (static_cast<uint32_t>(e.AllowedOperations() & DataPackageOperation::Copy) == 0
+                || !dataView.Contains(StandardDataFormats::StorageItems())) {
+                co_return;
+            }
 
-        const auto& items = co_await dataView.GetStorageItemsAsync();
-        co_await ImportFiles(items);
+            const auto& items = co_await dataView.GetStorageItemsAsync();
+            co_await ImportFiles(items);
+        }
+        catch (...)
+        {
+            UI::NotifyException(L"Pasting files");
+        }
     }
 
     void MainPage::WindowWidth_CurrentStateChanged(IInspectable const&, VisualStateChangedEventArgs const& e)
@@ -417,88 +502,106 @@ namespace winrt::Maple_App::implementation
 
     fire_and_forget MainPage::GenerateProfileButton_Click(IInspectable const& sender, RoutedEventArgs const& e)
     {
-        const auto lifetime = get_strong();
-        const auto& profile = VpnPlugInProfile{};
-        profile.AlwaysOn(false);
-        profile.ProfileName(L"Maple");
-        profile.RequireVpnClientAppUI(true);
-        profile.VpnPluginPackageFamilyName(Windows::ApplicationModel::Package::Current().Id().FamilyName());
-        profile.RememberCredentials(false);
-        profile.ServerUris().Append(Uri{ L"https://github.com/YtFlow/Maple" });
-        const auto& result = co_await VpnMgmtAgent.AddProfileFromObjectAsync(profile);
-        if (result == VpnManagementErrorStatus::Ok) {
-            co_await NotifyUser(L"Profile generated.");
+        try {
+            const auto lifetime = get_strong();
+            const auto& profile = VpnPlugInProfile{};
+            profile.AlwaysOn(false);
+            profile.ProfileName(L"Maple");
+            profile.RequireVpnClientAppUI(true);
+            profile.VpnPluginPackageFamilyName(Windows::ApplicationModel::Package::Current().Id().FamilyName());
+            profile.RememberCredentials(false);
+            profile.ServerUris().Append(Uri{ L"https://github.com/YtFlow/Maple" });
+            const auto& result = co_await VpnMgmtAgent.AddProfileFromObjectAsync(profile);
+            if (result == VpnManagementErrorStatus::Ok) {
+                co_await NotifyUser(L"Profile generated.");
+            }
+            else {
+                co_await NotifyUser(L"Failed to generate a profile (" + to_hstring(static_cast<int32_t>(result)) + L").");
+            }
         }
-        else {
-            co_await NotifyUser(L"Failed to generate a profile (" + to_hstring(static_cast<int32_t>(result)) + L").");
+        catch (...)
+        {
+            UI::NotifyException(L"Generating profile");
         }
     }
 
     fire_and_forget MainPage::ConnectionToggleSwitch_Toggled(IInspectable const&, RoutedEventArgs const&)
     {
-        const auto lifetime{ get_strong() };
+        try {
+            const auto lifetime{ get_strong() };
 
-        if (!ApplicationData::Current().LocalSettings().Values().HasKey(NETIF_SETTING_KEY)) {
-            MainPivot().SelectedIndex(1);
-            co_await 400ms;
-            co_await resume_foreground(Dispatcher());
-            NetifCombobox().IsDropDownOpen(true);
-            co_return;
-        }
+            if (!ApplicationData::Current().LocalSettings().Values().HasKey(NETIF_SETTING_KEY)) {
+                MainPivot().SelectedIndex(1);
+                co_await 400ms;
+                co_await resume_foreground(Dispatcher());
+                NetifCombobox().IsDropDownOpen(true);
+                co_return;
+            }
 
-        const auto connect = ConnectionToggleSwitch().IsOn();
-        ConnectionToggleSwitch().IsEnabled(false);
-        VpnManagementErrorStatus status = VpnManagementErrorStatus::Ok;
-        if (connect) {
-            status = co_await VpnMgmtAgent.ConnectProfileAsync(m_vpnProfile);
-        }
-        else {
-            status = co_await VpnMgmtAgent.DisconnectProfileAsync(m_vpnProfile);
+            const auto connect = ConnectionToggleSwitch().IsOn();
+            ConnectionToggleSwitch().IsEnabled(false);
+            VpnManagementErrorStatus status = VpnManagementErrorStatus::Ok;
+            if (connect) {
+                status = co_await VpnMgmtAgent.ConnectProfileAsync(m_vpnProfile);
+            }
+            else {
+                status = co_await VpnMgmtAgent.DisconnectProfileAsync(m_vpnProfile);
+            }
+            if (status == VpnManagementErrorStatus::Ok)
+            {
+                ConnectionToggleSwitch().IsEnabled(true);
+            }
+            else {
+                NotifyUser(L"Could not perform the requested operation. Please try again from system VPN settings for detailed error messages.");
+            }
         }
-        if (status == VpnManagementErrorStatus::Ok)
+        catch (...)
         {
-            ConnectionToggleSwitch().IsEnabled(true);
-        }
-        else {
-            NotifyUser(L"Could not perform the requested operation. Please try again from system VPN settings for detailed error messages.");
+            UI::NotifyException(L"Connecting");
         }
     }
     fire_and_forget MainPage::StartConnectionCheck()
     {
-        const auto lifetime{ get_strong() };
-        IVectorView<IVpnProfile> profiles{ nullptr };
-
-        auto event_token{ ConnectionToggleSwitch().Toggled({ this, &MainPage::ConnectionToggleSwitch_Toggled }) };
-        while (true) {
-            if (m_vpnProfile == nullptr) {
-                profiles = co_await VpnMgmtAgent.GetProfilesAsync();
-                for (auto const p : profiles) {
-                    if (p.ProfileName() == L"Maple" || p.ProfileName() == L"maple") {
-                        m_vpnProfile = p.try_as<VpnPlugInProfile>();
-                        break;
+        try {
+            const auto lifetime{ get_strong() };
+            IVectorView<IVpnProfile> profiles{ nullptr };
+
+            auto event_token{ ConnectionToggleSwitch().Toggled({ this, &MainPage::ConnectionToggleSwitch_Toggled }) };
+            while (true) {
+                if (m_vpnProfile == nullptr) {
+                    profiles = co_await VpnMgmtAgent.GetProfilesAsync();
+                    for (auto const p : profiles) {
+                        if (p.ProfileName() == L"Maple" || p.ProfileName() == L"maple") {
+                            m_vpnProfile = p.try_as<VpnPlugInProfile>();
+                            break;
+                        }
                     }
                 }
-            }
-            if (m_vpnProfile == nullptr) {
-                ConnectionToggleSwitch().IsEnabled(false);
-            }
-            else {
-                ToolTipService::SetToolTip(ConnectionToggleSwitchContainer(), nullptr);
-                auto status = VpnManagementConnectionStatus::Disconnected;
-                try {
-                    status = m_vpnProfile.ConnectionStatus();
+                if (m_vpnProfile == nullptr) {
+                    ConnectionToggleSwitch().IsEnabled(false);
                 }
-                catch (...) {}
-
-                ConnectionToggleSwitch().IsEnabled(status == VpnManagementConnectionStatus::Connected
-                    || status == VpnManagementConnectionStatus::Disconnected);
-                ConnectionToggleSwitch().Toggled(event_token);
-                ConnectionToggleSwitch().IsOn(status == VpnManagementConnectionStatus::Connected
-                    || status == VpnManagementConnectionStatus::Connecting);
-                event_token = ConnectionToggleSwitch().Toggled({ this, &MainPage::ConnectionToggleSwitch_Toggled });
+                else {
+                    ToolTipService::SetToolTip(ConnectionToggleSwitchContainer(), nullptr);
+                    auto status = VpnManagementConnectionStatus::Disconnected;
+                    try {
+                        status = m_vpnProfile.ConnectionStatus();
+                    }
+                    catch (...) {}
+
+                    ConnectionToggleSwitch().IsEnabled(status == VpnManagementConnectionStatus::Connected
+                        || status == VpnManagementConnectionStatus::Disconnected);
+                    ConnectionToggleSwitch().Toggled(event_token);
+                    ConnectionToggleSwitch().IsOn(status == VpnManagementConnectionStatus::Connected
+                        || status == VpnManagementConnectionStatus::Connecting);
+                    event_token = ConnectionToggleSwitch().Toggled({ this, &MainPage::ConnectionToggleSwitch_Toggled });
+                }
+                co_await 1s;
+                co_await resume_foreground(Dispatcher());
             }
-            co_await 1s;
-            co_await resume_foreground(Dispatcher());
+        }
+        catch (...)
+        {
+            UI::NotifyException(L"Checking VPN status");
         }
     }
 }

+ 8 - 6
Maple.App/Maple.App.vcxproj

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.props')" />
+  <Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.props')" />
   <Import Project="..\packages\Microsoft.UI.Xaml.2.8.1\build\native\Microsoft.UI.Xaml.props" Condition="Exists('..\packages\Microsoft.UI.Xaml.2.8.1\build\native\Microsoft.UI.Xaml.props')" />
   <PropertyGroup Label="Globals">
     <CppWinRTOptimized>true</CppWinRTOptimized>
@@ -188,6 +188,7 @@
     <ClInclude Include="MainPage.h">
       <DependentUpon>MainPage.xaml</DependentUpon>
     </ClInclude>
+    <ClInclude Include="UI.h" />
   </ItemGroup>
   <ItemGroup>
     <ApplicationDefinition Include="App.xaml">
@@ -313,6 +314,7 @@
       <DependentUpon>MainPage.xaml</DependentUpon>
     </ClCompile>
     <ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
+    <ClCompile Include="UI.cpp" />
   </ItemGroup>
   <ItemGroup>
     <Midl Include="App.idl">
@@ -380,8 +382,8 @@
   <ImportGroup Label="ExtensionTargets">
     <Import Project="..\packages\Microsoft.UI.Xaml.2.8.1\build\native\Microsoft.UI.Xaml.targets" Condition="Exists('..\packages\Microsoft.UI.Xaml.2.8.1\build\native\Microsoft.UI.Xaml.targets')" />
     <Import Project="..\packages\nlohmann.json.3.11.2\build\native\nlohmann.json.targets" Condition="Exists('..\packages\nlohmann.json.3.11.2\build\native\nlohmann.json.targets')" />
-    <Import Project="..\packages\Microsoft.Web.WebView2.1.0.1370.28\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\packages\Microsoft.Web.WebView2.1.0.1370.28\build\native\Microsoft.Web.WebView2.targets')" />
-    <Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.targets')" />
+    <Import Project="..\packages\Microsoft.Web.WebView2.1.0.1418.22\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\packages\Microsoft.Web.WebView2.1.0.1418.22\build\native\Microsoft.Web.WebView2.targets')" />
+    <Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
   </ImportGroup>
   <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
     <PropertyGroup>
@@ -390,9 +392,9 @@
     <Error Condition="!Exists('..\packages\Microsoft.UI.Xaml.2.8.1\build\native\Microsoft.UI.Xaml.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.UI.Xaml.2.8.1\build\native\Microsoft.UI.Xaml.props'))" />
     <Error Condition="!Exists('..\packages\Microsoft.UI.Xaml.2.8.1\build\native\Microsoft.UI.Xaml.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.UI.Xaml.2.8.1\build\native\Microsoft.UI.Xaml.targets'))" />
     <Error Condition="!Exists('..\packages\nlohmann.json.3.11.2\build\native\nlohmann.json.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\nlohmann.json.3.11.2\build\native\nlohmann.json.targets'))" />
-    <Error Condition="!Exists('..\packages\Microsoft.Web.WebView2.1.0.1370.28\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Web.WebView2.1.0.1370.28\build\native\Microsoft.Web.WebView2.targets'))" />
-    <Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.props'))" />
-    <Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.targets'))" />
+    <Error Condition="!Exists('..\packages\Microsoft.Web.WebView2.1.0.1418.22\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Web.WebView2.1.0.1418.22\build\native\Microsoft.Web.WebView2.targets'))" />
+    <Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
+    <Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
   </Target>
   <Target Name="AddWildCardItems" AfterTargets="PrepareForBuild">
     <ItemGroup>

+ 2 - 0
Maple.App/Maple.App.vcxproj.filters

@@ -6,10 +6,12 @@
   <ItemGroup>
     <ClCompile Include="pch.cpp" />
     <ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
+    <ClCompile Include="UI.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="pch.h" />
     <ClInclude Include="leaf.h" />
+    <ClInclude Include="UI.h" />
   </ItemGroup>
   <ItemGroup>
     <Image Include="Assets\LockScreenLogo.scale-200.png" />

+ 129 - 89
Maple.App/MonacoEditPage.cpp

@@ -4,11 +4,16 @@
 #include "MonacoEditPage.g.cpp"
 #endif
 
+#include <winrt/Windows.Storage.h>
+#include <winrt/Windows.Storage.Streams.h>
 #include <winrt/Microsoft.Web.WebView2.Core.h>
 #include <nlohmann/json.hpp>
 
+#include "UI.h"
+
 using namespace winrt;
 using namespace Windows::Storage;
+using namespace Windows::Storage::Streams;
 using namespace Windows::UI::Xaml;
 
 namespace winrt::Maple_App::implementation
@@ -18,14 +23,16 @@ namespace winrt::Maple_App::implementation
         InitializeComponent();
     }
 
-    fire_and_forget MonacoEditPage::Page_Loaded(IInspectable const& sender, RoutedEventArgs const& e)
+    fire_and_forget MonacoEditPage::Page_Loaded(IInspectable const&, RoutedEventArgs const&)
     {
-        auto const lifetime{ get_strong() };
-        co_await initializeWebView();
-    }
-    void MonacoEditPage::Page_Unloaded(IInspectable const& sender, RoutedEventArgs const& e)
-    {
-        //
+        try {
+            auto const lifetime{ get_strong() };
+            co_await initializeWebView();
+        }
+        catch (...)
+        {
+            UI::NotifyException(L"Initializing WebView");
+        }
     }
     IAsyncAction MonacoEditPage::initializeWebView() {
         auto const lifetime{ get_strong() };
@@ -46,119 +53,152 @@ namespace winrt::Maple_App::implementation
             configPath,
             Microsoft::Web::WebView2::Core::CoreWebView2HostResourceAccessKind::Allow
         );
-        webview.Source(Uri{ L"http://maple-monaco-editor-app-root.com/MonacoEditor/editor.html" });
+        webview.Source(Uri{ WEBVIEW_EDITOR_URL });
     }
 
     fire_and_forget MonacoEditPage::OnNavigatedTo(NavigationEventArgs const& e) {
-        auto const lifetime{ get_strong() };
-        auto const param = e.Parameter().as<Maple_App::ConfigViewModel>();
-        hstring fileName;
-        try
-        {
-            fileName = param.File().Name();
-        }
-        catch (...) {}
-        if (fileName == L"")
-        {
-            co_return;
+        try {
+            auto const lifetime{ get_strong() };
+            auto const param = e.Parameter().as<Maple_App::ConfigViewModel>();
+            hstring fileName;
+            try
+            {
+                fileName = param.File().Name();
+            }
+            catch (...) {}
+            if (fileName.empty())
+            {
+                co_return;
+            }
+            m_currentFileName = fileName;
+            switch (m_webviewState)
+            {
+            case MonacoEditPageWebViewState::Uninitialized:
+                co_await initializeWebView();
+                break;
+            case MonacoEditPageWebViewState::AwaitingEditorReady:
+                break;
+            case MonacoEditPageWebViewState::EditorReady:
+                co_await lifetime->WebView().ExecuteScriptAsync(hstring{ L"window.mapleHostApi.loadFile(`http://maple-monaco-editor-config-root.com/" } +
+                    fileName +
+                    L"`)");
+                break;
+            }
         }
-        m_currentFileName = fileName;
-        switch (m_webviewState)
+        catch (...)
         {
-        case MonacoEditPageWebViewState::Uninitialized:
-            co_await initializeWebView();
-            break;
-        case MonacoEditPageWebViewState::AwaitingEditorReady:
-            break;
-        case MonacoEditPageWebViewState::EditorReady:
-            co_await lifetime->WebView().ExecuteScriptAsync(hstring{ L"window.mapleHostApi.loadFile(`http://maple-monaco-editor-config-root.com/" } +
-                fileName +
-                L"`)");
-            break;
+            UI::NotifyException(L"Loading WebView page");
         }
     }
 
-    fire_and_forget MonacoEditPage::OnNavigatedFrom(NavigationEventArgs const& e)
-    {
-        // Although editor page will trigger a save before loading new files, there are cases where it does not load a
-        // new file, e.g. user selected a .mmdb file.
-        // TODO: trigger save
-        co_return;
-    }
-
     fire_and_forget MonacoEditPage::WebView_WebMessageReceived(
         MUXC::WebView2 const& sender,
         CoreWebView2WebMessageReceivedEventArgs const& args
     )
     {
-        auto const lifetime{ get_strong() };
-        nlohmann::json doc;
-        bool hasError{};
         try {
-            doc = nlohmann::json::parse(to_string(args.WebMessageAsJson()));
-        }
-        catch (...)
-        {
-            hasError = true;
-        }
-        if (hasError)
-        {
-            co_return;
-        }
+            auto const lifetime{ get_strong() };
 
-        std::string const& cmd = doc["cmd"];
-        if (cmd == "editorReady")
-        {
-            m_webviewState = MonacoEditPageWebViewState::EditorReady;
-            co_await WebView().ExecuteScriptAsync(hstring{ L"window.mapleHostApi.loadFile(`" } +
-                CONFIG_ROOT_VIRTUAL_HOSTW +
-                m_currentFileName +
-                L"`)");
-            SaveModifiedContent = [weak = weak_ref{ lifetime }]()->IAsyncAction
+            auto const source = args.Source();
+            if (source != WEBVIEW_EDITOR_URL)
+            {
+                co_return;
+            }
+
+            nlohmann::json doc;
+            bool hasError{};
+            try {
+                doc = nlohmann::json::parse(to_string(args.WebMessageAsJson()));
+            }
+            catch (...)
+            {
+                hasError = true;
+            }
+            if (hasError)
+            {
+                co_return;
+            }
+
+            std::string const& cmd = doc["cmd"];
+            if (cmd == "editorReady")
             {
-                if (auto const lifetime{ weak.get() })
+                m_webviewState = MonacoEditPageWebViewState::EditorReady;
+                co_await WebView().ExecuteScriptAsync(hstring{ L"window.mapleHostApi.loadFile(`" } +
+                    CONFIG_ROOT_VIRTUAL_HOSTW +
+                    m_currentFileName +
+                    L"`)");
+                SaveModifiedContent = [weak = weak_ref{ lifetime }]()->IAsyncAction
                 {
-                    struct awaiter : std::suspend_always
+                    if (auto const lifetime{ weak.get() })
                     {
-                        void await_suspend(
-                            std::coroutine_handle<> handle)
+                        struct awaiter : std::suspend_always
                         {
-                            lifetime->m_fileSaveHandle = handle;
-                        }
+                            void await_suspend(
+                                std::coroutine_handle<> handle)
+                            {
+                                lifetime->m_fileSaveHandle = handle;
+                            }
 
-                        com_ptr<MonacoEditPage> lifetime;
-                    };
-                    lifetime->WebView().ExecuteScriptAsync(hstring{ L"window.mapleHostApi.requestSaveCurrent()" });
-                    co_await awaiter{ .lifetime = lifetime };
+                            com_ptr<MonacoEditPage> lifetime;
+                        };
+                        lifetime->WebView().ExecuteScriptAsync(hstring{ L"window.mapleHostApi.requestSaveCurrent()" });
+                        co_await awaiter{ .lifetime = lifetime };
+                    }
+                    co_return;
+                };
+            }
+            else if (cmd == "save")
+            {
+                std::string path{ doc["path"] };
+                if (path == m_currentSavingFileName)
+                {
+                    co_return;
                 }
-                co_return;
-            };
-        }
-        else if (cmd == "save")
-        {
-            try {
+                m_currentSavingFileName = path;
+
                 auto const configDir{ co_await ApplicationData::Current().LocalFolder().CreateFolderAsync(L"config", CreationCollisionOption::OpenIfExists) };
                 auto const configDirPath{ to_string(configDir.Path()) + "\\" };
-                std::string path{ doc["path"] };
                 if (auto const pos{ path.find(CONFIG_ROOT_VIRTUAL_HOST) }; pos != std::string::npos)
                 {
                     path = path.replace(pos, strlen(CONFIG_ROOT_VIRTUAL_HOST), configDirPath);
                 }
                 auto const file{ co_await StorageFile::GetFileFromPathAsync(to_hstring(path)) };
                 std::string const data{ doc["text"] };
-                co_await FileIO::WriteBytesAsync(file, array_view{
-                    reinterpret_cast<uint8_t const*>(data.data()),
-                    static_cast<uint32_t>(data.size())
-                    });
-            }
-            catch (...) {}
-            co_await resume_foreground(Dispatcher());
-            if (auto const fileSaveHandle{ std::exchange(m_fileSaveHandle, nullptr) }) {
-                fileSaveHandle();
+                auto const fstream = co_await file.OpenAsync(FileAccessMode::ReadWrite, StorageOpenOptions::AllowOnlyReaders);
+                fstream.Size(0);
+                try {
+                    DataWriter const wr(fstream);
+                    try {
+                        wr.WriteBytes(array_view(reinterpret_cast <uint8_t const*>(data.data()), data.size()));
+                        co_await wr.StoreAsync();
+                        co_await fstream.FlushAsync().as<IAsyncOperation<bool>>();
+                        wr.Close();
+                    }
+                    catch (...)
+                    {
+                        wr.Close();
+                        throw;
+                    }
+                    fstream.Close();
+                }
+                catch (...)
+                {
+                    fstream.Close();
+                    throw;
+                }
+                co_await resume_foreground(Dispatcher());
+                if (auto const fileSaveHandle{ std::exchange(m_fileSaveHandle, nullptr) }) {
+                    fileSaveHandle();
+                }
             }
+            m_currentSavingFileName = "";
+            co_return;
+        }
+        catch (...)
+        {
+            m_currentSavingFileName = "";
+            UI::NotifyException(L"Processing web messages");
         }
-        co_return;
-
     }
 
 }

+ 2 - 2
Maple.App/MonacoEditPage.h

@@ -14,6 +14,7 @@ namespace winrt::Maple_App::implementation
 {
     constexpr wchar_t const* CONFIG_ROOT_VIRTUAL_HOSTW = L"http://maple-monaco-editor-config-root.com/";
     constexpr char const* CONFIG_ROOT_VIRTUAL_HOST = "http://maple-monaco-editor-config-root.com/";
+    constexpr wchar_t const* WEBVIEW_EDITOR_URL = L"http://maple-monaco-editor-app-root.com/MonacoEditor/editor.html";
     enum class MonacoEditPageWebViewState
     {
         Uninitialized,
@@ -25,9 +26,7 @@ namespace winrt::Maple_App::implementation
         MonacoEditPage();
 
         fire_and_forget OnNavigatedTo(NavigationEventArgs const& e);
-        fire_and_forget OnNavigatedFrom(NavigationEventArgs const& e);
         fire_and_forget Page_Loaded(IInspectable const& sender, RoutedEventArgs const& e);
-        void Page_Unloaded(IInspectable const& sender, RoutedEventArgs const& e);
         fire_and_forget WebView_WebMessageReceived(MUXC::WebView2 const& sender, CoreWebView2WebMessageReceivedEventArgs const& args);
 
         inline static std::function<IAsyncAction()> SaveModifiedContent{ nullptr };
@@ -40,6 +39,7 @@ namespace winrt::Maple_App::implementation
         MonacoEditPageWebViewState m_webviewState{ MonacoEditPageWebViewState::Uninitialized };
         std::coroutine_handle<> m_fileSaveHandle{ nullptr };
         hstring m_currentFileName{};
+        std::string m_currentSavingFileName{};
     };
 }
 

+ 4 - 0
Maple.App/MonacoEditor/src/init.ts

@@ -85,6 +85,10 @@ function initLang() {
             }
         } else {
             const res = await fetch(url)
+            if (!res.ok) {
+                console.error('failed to load file', url, res.status, res.statusText)
+                return
+            }
             const text = await res.text()
 
             let model: monaco.editor.ITextModel

+ 77 - 0
Maple.App/UI.cpp

@@ -0,0 +1,77 @@
+#include "pch.h"
+#include "UI.h"
+
+namespace winrt::Maple_App::implementation
+{
+    using Windows::UI::Xaml::Controls::ContentDialog;
+    using Windows::UI::Xaml::Input::StandardUICommand;
+    using Windows::UI::Xaml::Input::StandardUICommandKind;
+    using Windows::Foundation::Metadata::ApiInformation;
+
+    void UI::NotifyUser(hstring msg, hstring title)
+    {
+        static Windows::UI::Core::CoreDispatcher dispatcher{ Windows::UI::Xaml::Window::Current().Dispatcher() };
+        static std::vector<std::pair<hstring, hstring>> messages{};
+        dispatcher.RunAsync(Windows::UI::Core::CoreDispatcherPriority::Normal,
+            [msg = std::move(msg), title = std::move(title)]() -> fire_and_forget {
+                messages.push_back(std::make_pair(msg, title));
+
+        static bool isQueueRunning{ false };
+        if (std::exchange(isQueueRunning, true))
+        {
+            co_return;
+        }
+
+        // Display all messages until the queue is drained.
+        while (messages.size() > 0)
+        {
+            auto it = messages.begin();
+            auto [message, messageTitle] = std::move(*it);
+            messages.erase(it);
+            ContentDialog dialog;
+            if (messageTitle.size() > 0)
+            {
+                dialog.Title(box_value(std::move(messageTitle)));
+            }
+            if (ApiInformation::IsTypePresent(L"Windows.UI.Xaml.Input.StandardUICommand"))
+            {
+                dialog.CloseButtonCommand(StandardUICommand(StandardUICommandKind::Close));
+            }
+            else if (ApiInformation::IsPropertyPresent(
+                L"Windows.UI.Xaml.Controls.ContentDialog", L"CloseButtonText"))
+            {
+                dialog.CloseButtonText(L"Close");
+            }
+            else
+            {
+                dialog.SecondaryButtonText(L"Close");
+            }
+            dialog.Content(box_value(message));
+
+            co_await dialog.ShowAsync();
+        }
+        isQueueRunning = false;
+            });
+    }
+
+    void UI::NotifyUser(char const* msg, hstring title)
+    {
+        NotifyUser(to_hstring(msg), std::move(title));
+    }
+
+    void UI::NotifyException(std::wstring_view context)
+    {
+        try
+        {
+            throw;
+        }
+        catch (hresult_error const& hr)
+        {
+            NotifyUser(hr.message(), hstring{ L"Error occurred: " } + context);
+        }
+        catch (std::exception const& ex)
+        {
+            NotifyUser(ex.what(), hstring{ L"Unexpected exception: " } + context);
+        }
+    }
+}

+ 12 - 0
Maple.App/UI.h

@@ -0,0 +1,12 @@
+#pragma once
+
+namespace winrt::Maple_App::implementation
+{
+    struct UI
+    {
+        static void NotifyUser(hstring msg, hstring title);
+        static void NotifyUser(char const* msg, hstring title);
+        static void NotifyException(std::wstring_view context);
+    };
+}
+

+ 4 - 4
Maple.Task/Maple.Task.vcxproj

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.props')" />
+  <Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.props')" />
   <PropertyGroup Label="Globals">
     <CppWinRTOptimized>true</CppWinRTOptimized>
     <CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
@@ -170,13 +170,13 @@
   </ItemGroup>
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
   <ImportGroup Label="ExtensionTargets">
-    <Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.targets')" />
+    <Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
   </ImportGroup>
   <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
     <PropertyGroup>
       <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
     </PropertyGroup>
-    <Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.props'))" />
-    <Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.targets'))" />
+    <Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
+    <Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.221117.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
   </Target>
 </Project>

+ 46 - 28
Maple.Task/VpnPlugin.cpp

@@ -4,9 +4,10 @@
 #include "winrt/Windows.Storage.Streams.h"
 
 extern "C" void* lwip_strerr(uint8_t) {
-    return "";
+    return (void*)(const char*)"";
 }
 
+
 namespace winrt::Maple_Task::implementation
 {
     using Windows::Networking::HostName;
@@ -15,7 +16,47 @@ namespace winrt::Maple_Task::implementation
     using namespace Windows::Networking::Vpn;
     using namespace Windows::Storage;
 
+    extern "C" {
+        typedef void(__cdecl* netstack_cb)(uint8_t*, size_t, void*);
+        void cb(uint8_t* data, size_t size, void* outputStreamAbi) {
+            bool needSendDummyBuffer = false;
+            {
+                std::lock_guard _guard{ VpnPluginInstance->m_decapQueueLock };
+                auto& q = VpnPluginInstance->m_decapQueue;
+                {
+                    std::vector<uint8_t> buf(size);
+                    if (memcpy_s(buf.data(), buf.capacity(), data, size)) {
+                        return;
+                    }
+                    needSendDummyBuffer = q.empty();
+                    q.emplace(buf);
+                }
+            }
+            if (!needSendDummyBuffer) {
+                return;
+            }
+            IOutputStream outputStream{ nullptr };
+            winrt::attach_abi(outputStream, outputStreamAbi);
+            try {
+                const auto _ = outputStream.WriteAsync(dummyBuffer);
+            }
+            catch (...) {}
+            winrt::detach_abi(outputStream);
+        }
+    }
     void VpnPlugin::Connect(VpnChannel const& channel)
+    {
+        try
+        {
+            ConnectCore(channel);
+        }
+        catch (std::exception const& ex)
+        {
+            channel.TerminateConnection(to_hstring(ex.what()));
+        }
+    }
+
+    void VpnPlugin::ConnectCore(VpnChannel const& channel)
     {
         const auto localhost = HostName{ L"127.0.0.1" };
         DatagramSocket transport{}, backTransport{};
@@ -28,12 +69,12 @@ namespace winrt::Maple_Task::implementation
         VpnRouteAssignment routeScope{};
         routeScope.ExcludeLocalSubnets(true);
         routeScope.Ipv4InclusionRoutes(std::vector<VpnRoute>{
-            // 直接写 0.0.0.0/0 哪怕绑了接口也会绕回环
+            // 鐩存帴鍐� 0.0.0.0/0 鍝�€曠粦浜嗘帴鍙d篃浼氱粫鍥炵幆
             // VpnRoute(HostName{ L"0.0.0.0" }, 0)
             VpnRoute(HostName{ L"0.0.0.0" }, 1),
                 VpnRoute(HostName{ L"128.0.0.0" }, 1),
         });
-        // 排除代理服务器的话就会 os 10023 以一种访问权限不允许的方式做了一个访问套接字的尝试
+        // 鎺掗櫎浠g悊鏈嶅姟鍣ㄧ殑璇濆氨浼� os 10023 浠ヤ竴绉嶈�闂�潈闄愪笉鍏佽�鐨勬柟寮忓仛浜嗕竴涓��闂��鎺ュ瓧鐨勫皾璇�
         // routeScope.Ipv4ExclusionRoutes(std::vector<VpnRoute>{
         //     VpnRoute(HostName{ L"172.25.0.0" }, 16)
         // });
@@ -47,31 +88,8 @@ namespace winrt::Maple_Task::implementation
             }
         }
         m_backTransport = backTransport;
-        m_netStackHandle = netstack_register([](uint8_t* data, size_t size, void* outputStreamAbi) {
-            bool needSendDummyBuffer = false;
-            {
-                std::lock_guard _guard{ VpnPluginInstance->m_decapQueueLock };
-                auto& q = VpnPluginInstance->m_decapQueue;
-                {
-                    std::vector<uint8_t> buf(size);
-                    if (memcpy_s(buf.data(), buf.capacity(), data, size)) {
-                        return;
-                    }
-                    needSendDummyBuffer = q.empty();
-                    q.emplace(buf);
-                }
-            }
-            if (!needSendDummyBuffer) {
-                return;
-            }
-            IOutputStream outputStream{ nullptr };
-            winrt::attach_abi(outputStream, outputStreamAbi);
-            try {
-                const auto _ = outputStream.WriteAsync(dummyBuffer);
-            }
-            catch (...) {}
-            winrt::detach_abi(outputStream);
-            }, outputStreamAbi);
+
+        m_netStackHandle = netstack_register(cb, outputStreamAbi);
         if (m_netStackHandle == nullptr) {
             channel.TerminateConnection(L"Error initializing Leaf netstack.");
             return;

+ 1 - 0
Maple.Task/VpnPlugin.h

@@ -21,6 +21,7 @@ namespace winrt::Maple_Task::implementation
         void Decapsulate(Windows::Networking::Vpn::VpnChannel const& channel, Windows::Networking::Vpn::VpnPacketBuffer const& encapBuffer, Windows::Networking::Vpn::VpnPacketBufferList const& decapsulatedPackets, Windows::Networking::Vpn::VpnPacketBufferList const& controlPacketsToSend);
 
     private:
+        void ConnectCore(Windows::Networking::Vpn::VpnChannel const& channel);
         void StopLeaf();
 
         Leaf* m_leaf{};