Bläddra i källkod

Add custom config folder UI

VPN and Leaf are not done yet.
bdbai 2 år sedan
förälder
incheckning
48127f665d

+ 63 - 0
Maple.App/ConfigUtil.cpp

@@ -0,0 +1,63 @@
+#include "pch.h"
+#include "ConfigUtil.h"
+
+#include <winrt\Windows.Storage.AccessCache.h>
+
+namespace winrt::Maple_App::implementation {
+
+    using namespace Windows::Foundation;
+    using namespace Windows::Storage;
+    using namespace Windows::Storage::AccessCache;
+
+    ApplicationData ConfigUtil::GetCurrentAppData()
+    {
+        static ApplicationData currentAppData{ ApplicationData::Current() };
+        return currentAppData;
+    }
+    IAsyncOperation<StorageFolder> ConfigUtil::GetDefaultConfigFolder()
+    {
+        auto configItem = co_await LocalFolder.TryGetItemAsync(LocalFolderConfigDirName);
+        if (configItem == nullptr || configItem.IsOfType(StorageItemTypes::File)) {
+            configItem = co_await LocalFolder.CreateFolderAsync(LocalFolderConfigDirName, CreationCollisionOption::ReplaceExisting);
+        }
+        co_return configItem.as<StorageFolder>();
+    }
+    IAsyncOperation<IStorageFolder> ConfigUtil::GetConfigFolder()
+    {
+        if (cachedConfigFolder)
+        {
+            co_return cachedConfigFolder;
+        }
+
+        StorageFolder folder{ nullptr };
+        try {
+            folder = co_await StorageApplicationPermissions::FutureAccessList().GetFolderAsync(ConfigFolderAccessListKey);
+        }
+        catch (hresult_invalid_argument const&) {}
+        usingDefaultConfigFolder = !static_cast<bool>(folder);
+        if (!folder)
+        {
+            folder = co_await GetDefaultConfigFolder();
+        }
+        cachedConfigFolder = folder;
+        co_return folder;
+    }
+
+    void ConfigUtil::SetConfigFolder(IStorageFolder folder)
+    {
+        if (folder) {
+            StorageApplicationPermissions::FutureAccessList().AddOrReplace(ConfigFolderAccessListKey, folder);
+        }
+        else
+        {
+            StorageApplicationPermissions::FutureAccessList().Remove(ConfigFolderAccessListKey);
+        }
+        cachedConfigFolder = nullptr;
+        usingDefaultConfigFolder = !static_cast<bool>(folder);
+    }
+
+    bool ConfigUtil::UsingDefaultConfigFolder() noexcept {
+        return usingDefaultConfigFolder;
+    }
+}
+

+ 22 - 0
Maple.App/ConfigUtil.h

@@ -0,0 +1,22 @@
+#pragma once
+
+namespace winrt::Maple_App::implementation {
+    struct ConfigUtil
+    {
+        static Windows::Storage::ApplicationData GetCurrentAppData();
+        static Windows::Foundation::IAsyncOperation<Windows::Storage::IStorageFolder> GetConfigFolder();
+        static Windows::Foundation::IAsyncOperation<Windows::Storage::StorageFolder> GetDefaultConfigFolder();
+        static void SetConfigFolder(Windows::Storage::IStorageFolder folder);
+        static bool UsingDefaultConfigFolder() noexcept;
+
+        inline const static auto LocalFolder{ GetCurrentAppData().LocalFolder() };
+
+    private:
+        constexpr static std::wstring_view ConfigFolderAccessListKey = L"configFolder";
+        constexpr static std::wstring_view LocalFolderConfigDirName = L"config";
+
+        static inline Windows::Storage::IStorageFolder cachedConfigFolder{ nullptr };
+        static inline bool usingDefaultConfigFolder{};
+    };
+}
+

+ 30 - 0
Maple.App/Converter/BoolToVisibilityConverter.cpp

@@ -0,0 +1,30 @@
+#include "pch.h"
+#include "Converter/BoolToVisibilityConverter.h"
+#include "BoolToVisibilityConverter.g.cpp"
+
+namespace winrt::Maple_App::implementation
+{
+    using namespace Windows::Foundation;
+    using winrt::Windows::UI::Xaml::Interop::TypeName;
+
+    IInspectable BoolToVisibilityConverter::Convert(
+        IInspectable const& value,
+        [[maybe_unused]] TypeName const& targetType,
+        IInspectable const& parameter,
+        [[maybe_unused]] hstring const& language
+    )
+    {
+        bool const rev = parameter.try_as<bool>().value_or(false);
+        bool const val = value.try_as<bool>().value_or(false);
+        return box_value(val ^ rev);
+    }
+    IInspectable BoolToVisibilityConverter::ConvertBack(
+        [[maybe_unused]] IInspectable const& value,
+        [[maybe_unused]] TypeName const& targetType,
+        [[maybe_unused]] IInspectable const& parameter,
+        [[maybe_unused]] hstring const& language
+    )
+    {
+        throw hresult_not_implemented();
+    }
+}

+ 19 - 0
Maple.App/Converter/BoolToVisibilityConverter.h

@@ -0,0 +1,19 @@
+#pragma once
+#include "BoolToVisibilityConverter.g.h"
+
+namespace winrt::Maple_App::implementation
+{
+    struct BoolToVisibilityConverter : BoolToVisibilityConverterT<BoolToVisibilityConverter>
+    {
+        BoolToVisibilityConverter() = default;
+
+        winrt::Windows::Foundation::IInspectable Convert(winrt::Windows::Foundation::IInspectable const& value, winrt::Windows::UI::Xaml::Interop::TypeName const& targetType, winrt::Windows::Foundation::IInspectable const& parameter, hstring const& language);
+        winrt::Windows::Foundation::IInspectable ConvertBack(winrt::Windows::Foundation::IInspectable const& value, winrt::Windows::UI::Xaml::Interop::TypeName const& targetType, winrt::Windows::Foundation::IInspectable const& parameter, hstring const& language);
+    };
+}
+namespace winrt::Maple_App::factory_implementation
+{
+    struct BoolToVisibilityConverter : BoolToVisibilityConverterT<BoolToVisibilityConverter, implementation::BoolToVisibilityConverter>
+    {
+    };
+}

+ 10 - 0
Maple.App/Converter/BoolToVisibilityConverter.idl

@@ -0,0 +1,10 @@
+namespace Maple_App
+{
+    [bindable]
+    [default_interface]
+    runtimeclass BoolToVisibilityConverter : Windows.UI.Xaml.Data.IValueConverter
+    {
+        BoolToVisibilityConverter();
+    }
+}
+

+ 90 - 35
Maple.App/MainPage.cpp

@@ -61,31 +61,46 @@ namespace winrt::Maple_App::implementation
     {
         return m_configItemsProperty;
     }
+    DependencyProperty MainPage::UsingDefaultConfigFolderProperty()
+    {
+        return m_usingDefaultConfigFolderProperty;
+    }
     IObservableVector<Maple_App::ConfigViewModel> MainPage::ConfigItems()
     {
         return GetValue(m_configItemsProperty).as<IObservableVector<Maple_App::ConfigViewModel>>();
     }
-
-    void MainPage::Page_Loaded(IInspectable const&, RoutedEventArgs const&)
+    bool MainPage::UsingDefaultConfigFolder()
     {
-        const auto _ = LoadConfigs();
-        NavigationManager = SystemNavigationManager::GetForCurrentView();
-        NavigationManager.AppViewBackButtonVisibility(MainSplitView().IsPaneOpen()
-            ? AppViewBackButtonVisibility::Collapsed
-            : AppViewBackButtonVisibility::Visible);
+        return GetValue(m_usingDefaultConfigFolderProperty).try_as<bool>().value_or(false);
+    }
 
-        const auto weakThis = get_weak();
-        NavigationManager.BackRequested([weakThis](const auto&, const auto&) {
-            if (const auto self{ weakThis.get() }) {
-                self->MainSplitView().IsPaneOpen(true);
-                const auto currentVisibility = NavigationManager.AppViewBackButtonVisibility();
-                if (currentVisibility == AppViewBackButtonVisibility::Visible) {
-                    NavigationManager.AppViewBackButtonVisibility(AppViewBackButtonVisibility::Disabled);
+    fire_and_forget MainPage::Page_Loaded(IInspectable const&, RoutedEventArgs const&)
+    {
+        try {
+            const auto loadConfigsTask = LoadConfigs();
+            NavigationManager = SystemNavigationManager::GetForCurrentView();
+            NavigationManager.AppViewBackButtonVisibility(MainSplitView().IsPaneOpen()
+                ? AppViewBackButtonVisibility::Collapsed
+                : AppViewBackButtonVisibility::Visible);
+
+            const auto weakThis = get_weak();
+            NavigationManager.BackRequested([weakThis](const auto&, const auto&) {
+                if (const auto self{ weakThis.get() }) {
+                    self->MainSplitView().IsPaneOpen(true);
+                    const auto currentVisibility = NavigationManager.AppViewBackButtonVisibility();
+                    if (currentVisibility == AppViewBackButtonVisibility::Visible) {
+                        NavigationManager.AppViewBackButtonVisibility(AppViewBackButtonVisibility::Disabled);
+                    }
                 }
-            }
-            });
+                });
 
-        StartConnectionCheck();
+            StartConnectionCheck();
+            co_await loadConfigsTask;
+        }
+        catch (...)
+        {
+            UI::NotifyException(L"Loading MainPage");
+        }
     }
 
     IAsyncAction MainPage::NotifyUser(const hstring& message) {
@@ -95,17 +110,6 @@ namespace winrt::Maple_App::implementation
         co_await dialog.ShowAsync();
     }
 
-    IAsyncOperation<IStorageFolder> MainPage::InitializeConfigFolder()
-    {
-        const auto& appData = ApplicationData::Current();
-        const auto& localFolder = appData.LocalFolder();
-        auto configItem = co_await localFolder.TryGetItemAsync(L"config");
-        if (configItem == nullptr || configItem.IsOfType(StorageItemTypes::File)) {
-            configItem = co_await localFolder.CreateFolderAsync(L"config", CreationCollisionOption::ReplaceExisting);
-        }
-        co_return configItem.as<IStorageFolder>();
-    }
-
     IAsyncOperation<StorageFile> MainPage::CopyDefaultConfig(const IStorageFolder& configFolder, std::wstring_view path, const hstring& desiredName)
     {
         const auto& defaultConfigSrc = co_await StorageFile::GetFileFromApplicationUriAsync(Uri{ path });
@@ -116,7 +120,9 @@ namespace winrt::Maple_App::implementation
     {
         const auto lifetime = get_strong();
         const auto& appData = ApplicationData::Current();
-        m_configFolder = co_await InitializeConfigFolder();
+        m_configFolder = co_await ConfigUtil::GetConfigFolder();
+        SetValue(m_usingDefaultConfigFolderProperty, box_value(ConfigUtil::UsingDefaultConfigFolder()));
+        CustomConfigFolderPathText().Text(m_configFolder.Path());
         auto configFiles = co_await m_configFolder.GetFilesAsync();
         if (configFiles.Size() == 0) {
             const auto& defaultConfigDst = co_await CopyDefaultConfig(m_configFolder, DEFAULT_CONF_FILE_PATH, L"default.conf");
@@ -127,6 +133,7 @@ namespace winrt::Maple_App::implementation
         std::vector<Maple_App::ConfigViewModel> configModels;
         configModels.reserve(static_cast<size_t>(configFiles.Size()));
 
+        m_defaultConfig = nullptr;
         const auto& defaultConfigPath = appData.LocalSettings().Values().TryLookup(CONFIG_PATH_SETTING_KEY).try_as<hstring>();
         for (const auto& file : configFiles) {
             const auto isDefault = file.Path() == defaultConfigPath;
@@ -170,9 +177,10 @@ namespace winrt::Maple_App::implementation
                 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;
+            if (auto const oldConfig{ std::exchange(m_defaultConfig, item) }) {
+                oldConfig.IsDefault(false);
+            }
         }
         catch (...)
         {
@@ -239,8 +247,8 @@ namespace winrt::Maple_App::implementation
             if (ConfigListView().SelectedItem() == item) {
                 ConfigListView().SelectedIndex(ConfigListView().SelectedIndex() == 0 ? 1 : 0);
             }
-            configItems.RemoveAt(index);
             co_await item.Delete();
+            configItems.RemoveAt(index);
             if (configItems.Size() == 0) {
                 LoadConfigs();
             }
@@ -340,9 +348,14 @@ namespace winrt::Maple_App::implementation
             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);
+            const auto file = item.File();
+            const auto parent = co_await file.GetParentAsync();
+            if (!parent)
+            {
+                UI::NotifyUser("Failed to load config folder.", L"Error: duplicating");
+                co_return;
+            }
+            const auto newFile = co_await file.CopyAsync(parent, file.Name(), NameCollisionOption::GenerateUniqueName);
             ConfigItems().Append(co_await ConfigViewModel::FromFile(newFile, false));
         }
         catch (...)
@@ -402,6 +415,12 @@ namespace winrt::Maple_App::implementation
     void MainPage::ConfigListView_SelectionChanged(IInspectable const&, SelectionChangedEventArgs const& e)
     {
         try {
+            if (e.AddedItems().Size() == 0 && e.RemovedItems().Size() > 0)
+            {
+                MainContentFrame().BackStack().Clear();
+                MainContentFrame().Content(nullptr);
+                return;
+            }
             const auto& item = e.AddedItems().First().Current();
             auto targetPage = xaml_typename<MonacoEditPage>();
             const auto& config = item.try_as<Maple_App::ConfigViewModel>();
@@ -603,4 +622,40 @@ namespace winrt::Maple_App::implementation
             UI::NotifyException(L"Checking VPN status");
         }
     }
+
+    fire_and_forget MainPage::ConfigFolderSelectButton_Click(IInspectable const&, RoutedEventArgs const&)
+    {
+        try
+        {
+            m_configFolderPicker.FileTypeFilter().Clear();
+            m_configFolderPicker.FileTypeFilter().Append(L"*");
+            auto folder = co_await m_configFolderPicker.PickSingleFolderAsync();
+            if (!folder)
+            {
+                co_return;
+            }
+            co_await folder.GetItemsAsync(); // Try to read something to see if is OK
+            ConfigUtil::SetConfigFolder(std::move(folder));
+            LoadConfigs();
+        }
+        catch (...)
+        {
+            UI::NotifyException(L"Select Config Folder");
+        }
+    }
+
+    void MainPage::ConfigFolderResetButton_Click(IInspectable const&, RoutedEventArgs const&)
+    {
+        try
+        {
+            ConfigUtil::SetConfigFolder(nullptr);
+            LoadConfigs();
+        }
+        catch (...)
+        {
+            UI::NotifyException(L"Reset Config Folder");
+        }
+    }
+
 }
+

+ 15 - 3
Maple.App/MainPage.h

@@ -4,6 +4,7 @@
 #include "Model/ConfigViewModel.h"
 
 #include <winrt/Windows.Networking.Vpn.h>
+#include "ConfigUtil.h"
 
 using namespace winrt;
 using namespace Windows::ApplicationModel::Core;
@@ -34,9 +35,11 @@ namespace winrt::Maple_App::implementation
         MainPage();
 
         static DependencyProperty ConfigItemsProperty();
+        static DependencyProperty UsingDefaultConfigFolderProperty();
         IObservableVector<Maple_App::ConfigViewModel> ConfigItems();
+        bool UsingDefaultConfigFolder();
 
-        void Page_Loaded(IInspectable const& sender, RoutedEventArgs const& e);
+        fire_and_forget Page_Loaded(IInspectable const& sender, RoutedEventArgs const& e);
         void CoreTitleBar_LayoutMetricsChanged(CoreApplicationViewTitleBar const& sender, IInspectable const& args);
         void CoreWindow_Activated(IInspectable const& sender, WindowActivatedEventArgs const& args);
         void ConfigSetAsDefaultMenuItem_Click(IInspectable const& sender, RoutedEventArgs const& e);
@@ -58,6 +61,8 @@ namespace winrt::Maple_App::implementation
         void MainSplitView_PaneClosing(SplitView const& sender, SplitViewPaneClosingEventArgs const& args);
         fire_and_forget GenerateProfileButton_Click(IInspectable const& sender, RoutedEventArgs const& e);
         fire_and_forget ConnectionToggleSwitch_Toggled(IInspectable const& sender, RoutedEventArgs const& e);
+        fire_and_forget ConfigFolderSelectButton_Click(IInspectable const& sender, RoutedEventArgs const& e);
+        void ConfigFolderResetButton_Click(IInspectable const& sender, RoutedEventArgs const& e);
 
     private:
         inline static DependencyProperty m_configItemsProperty =
@@ -67,6 +72,13 @@ namespace winrt::Maple_App::implementation
                 xaml_typename<Maple_App::MainPage>(),
                 nullptr
             );
+        inline static DependencyProperty m_usingDefaultConfigFolderProperty =
+            DependencyProperty::Register(
+                L"UsingDefaultConfigFolder",
+                xaml_typename<bool>(),
+                xaml_typename<Maple_App::MainPage>(),
+                nullptr
+            );
         inline static DependencyProperty m_renamedNameProperty =
             DependencyProperty::Register(
                 L"RenamedName",
@@ -80,9 +92,9 @@ namespace winrt::Maple_App::implementation
         IStorageFolder m_configFolder{ nullptr };
         Maple_App::ConfigViewModel m_defaultConfig{ nullptr };
         VpnPlugInProfile m_vpnProfile{ nullptr };
+        Pickers::FolderPicker m_configFolderPicker{};
 
         static IAsyncAction NotifyUser(const hstring& message);
-        static IAsyncOperation<IStorageFolder> InitializeConfigFolder();
         static IAsyncOperation<StorageFile> CopyDefaultConfig(const IStorageFolder& configFolder, std::wstring_view path, const hstring& desiredName);
 
         void RequestRenameItem(const Maple_App::ConfigViewModel& item);
@@ -94,7 +106,7 @@ namespace winrt::Maple_App::implementation
         template<ConvertableToIStorageItem T>
         IAsyncAction ImportFiles(const IVectorView<T>& items) {
             const auto lifetime = get_strong();
-            const auto& targetDir = co_await InitializeConfigFolder();
+            const auto& targetDir = co_await ConfigUtil::GetConfigFolder();
             // TODO: concurrency
             for (const auto& item : items) {
                 const auto& file = item.try_as<IStorageFile>();

+ 2 - 0
Maple.App/MainPage.idl

@@ -8,8 +8,10 @@ namespace Maple_App
         MainPage();
 
         static Windows.UI.Xaml.DependencyProperty ConfigItemsProperty{ get; };
+        static Windows.UI.Xaml.DependencyProperty UsingDefaultConfigFolderProperty{ get; };
 
         Windows.Foundation.Collections.IObservableVector<ConfigViewModel> ConfigItems{ get; };
+        Boolean UsingDefaultConfigFolder{ get; };
         Windows.UI.Xaml.Controls.ListView ConfigListView{ get; };
     }
 }

+ 47 - 1
Maple.App/MainPage.xaml

@@ -15,6 +15,7 @@
 
     <Page.Resources>
         <maple_app:DateTimeConverter x:Key="DateTimeConverter"/>
+        <maple_app:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
         <pickers:FileOpenPicker x:Key="ImportFilePicker" x:Name="ImportFilePicker"/>
     </Page.Resources>
 
@@ -211,12 +212,57 @@
                                 </TextBlock>
                                 <Button Content="Generate Profile" Click="GenerateProfileButton_Click"/>
 
+                                <TextBlock Margin="0, 18" Text="Config Folder" Style="{ThemeResource TitleTextBlockStyle}"/>
+                                <TextBlock
+                                    Margin="0, 0, 0, 8"
+                                    Text="Using app built-in local state."
+                                    TextWrapping="WrapWholeWords"
+                                    Visibility="{x:Bind UsingDefaultConfigFolder, Mode=OneWay}"/>
+                                <TextBlock
+                                    Margin="0, 0, 0, 18"
+                                    Text="Using custom configuration folder."
+                                    TextWrapping="WrapWholeWords"
+                                    Visibility="{x:Bind
+                                        UsingDefaultConfigFolder,
+                                        Converter={StaticResource BoolToVisibilityConverter},
+                                        ConverterParameter=x:True,
+                                        Mode=OneWay}"/>
+                                <TextBlock
+                                    Margin="0, 0, 0, 8"
+                                    Foreground="{ThemeResource SystemControlPageTextBaseMediumBrush}"
+                                    TextWrapping="WrapWholeWords">
+                                    Specify a folder for Maple and Leaf to find configuration files, external rule databases and TLS certificates.
+                                </TextBlock>
+                                <StackPanel Orientation="Horizontal">
+                                    <Button Content="Select Folder" Click="ConfigFolderSelectButton_Click"/>
+                                    <Button 
+                                        Margin="4, 0, 0, 0"
+                                        Content="Reset to Default"
+                                        Visibility="{x:Bind
+                                            UsingDefaultConfigFolder,
+                                            Mode=OneWay,
+                                            Converter={StaticResource BoolToVisibilityConverter},
+                                            ConverterParameter=x:True}"
+                                        Click="ConfigFolderResetButton_Click"/>
+                                </StackPanel>
+                                <TextBlock
+                                    x:Name="CustomConfigFolderPathText"
+                                    Margin="0, 4, 0, 0"
+                                    TextWrapping="Wrap"
+                                    IsTextSelectionEnabled="True"
+                                    Visibility="{x:Bind
+                                        UsingDefaultConfigFolder,
+                                        Converter={StaticResource BoolToVisibilityConverter},
+                                        ConverterParameter=x:True,
+                                        Mode=OneWay}"
+                                    />
+
                                 <TextBlock Margin="0, 18" Text="About" Style="{ThemeResource TitleTextBlockStyle}"/>
                                 <HyperlinkButton Padding="0" Content="Homepage" NavigateUri="https://github.com/YtFlow/Maple"/>
                                 <HyperlinkButton Padding="0" Content="Report Issues" NavigateUri="https://github.com/YtFlow/Maple/issues"/>
                                 <HyperlinkButton Padding="0" Content="License" NavigateUri="https://github.com/YtFlow/Maple/blob/main/LICENSE"/>
 
-                                <TextBlock Margin="0, 20, 0, 0" TextWrapping="WrapWholeWords">
+                                <TextBlock Margin="0, 20, 0, 40" TextWrapping="WrapWholeWords">
                                     <Run>This product contains a</Run>
                                     <Hyperlink NavigateUri="https://github.com/YtFlow/leaf" TextDecorations="None">
                                         <Run>modified version</Run>

+ 13 - 0
Maple.App/Maple.App.vcxproj

@@ -148,10 +148,15 @@
     </Link>
   </ItemDefinitionGroup>
   <ItemGroup>
+    <ClInclude Include="Converter\BoolToVisibilityConverter.h">
+      <DependentUpon>Converter\BoolToVisibilityConverter.idl</DependentUpon>
+      <SubType>Code</SubType>
+    </ClInclude>
     <ClInclude Include="CertPage.h">
       <DependentUpon>CertPage.xaml</DependentUpon>
       <SubType>Code</SubType>
     </ClInclude>
+    <ClInclude Include="ConfigUtil.h" />
     <ClInclude Include="Converter\DateTimeConverter.h">
       <DependentUpon>Converter\DateTimeConverter.idl</DependentUpon>
       <SubType>Code</SubType>
@@ -272,10 +277,15 @@
     <Image Include="Assets\Wide310x150Logo.scale-400.png" />
   </ItemGroup>
   <ItemGroup>
+    <ClCompile Include="Converter\BoolToVisibilityConverter.cpp">
+      <DependentUpon>Converter\BoolToVisibilityConverter.idl</DependentUpon>
+      <SubType>Code</SubType>
+    </ClCompile>
     <ClCompile Include="CertPage.cpp">
       <DependentUpon>CertPage.xaml</DependentUpon>
       <SubType>Code</SubType>
     </ClCompile>
+    <ClCompile Include="ConfigUtil.cpp" />
     <ClCompile Include="Converter\DateTimeConverter.cpp">
       <DependentUpon>Converter\DateTimeConverter.idl</DependentUpon>
       <SubType>Code</SubType>
@@ -320,6 +330,9 @@
     <Midl Include="App.idl">
       <DependentUpon>App.xaml</DependentUpon>
     </Midl>
+    <Midl Include="Converter\BoolToVisibilityConverter.idl">
+      <SubType>Designer</SubType>
+    </Midl>
     <Midl Include="CertPage.idl">
       <DependentUpon>CertPage.xaml</DependentUpon>
       <SubType>Code</SubType>

+ 92 - 21
Maple.App/Maple.App.vcxproj.filters

@@ -1,25 +1,32 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <ItemGroup>
-    <ApplicationDefinition Include="App.xaml" />
-  </ItemGroup>
   <ItemGroup>
     <ClCompile Include="pch.cpp" />
     <ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
-    <ClCompile Include="UI.cpp" />
+    <ClCompile Include="UI.cpp">
+      <Filter>Util</Filter>
+    </ClCompile>
+    <ClCompile Include="ConfigUtil.cpp">
+      <Filter>Util</Filter>
+    </ClCompile>
+    <ClCompile Include="Converter\BoolToVisibilityConverter.cpp">
+      <Filter>Converter</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="pch.h" />
     <ClInclude Include="leaf.h" />
-    <ClInclude Include="UI.h" />
+    <ClInclude Include="UI.h">
+      <Filter>Util</Filter>
+    </ClInclude>
+    <ClInclude Include="ConfigUtil.h">
+      <Filter>Util</Filter>
+    </ClInclude>
+    <ClInclude Include="Converter\BoolToVisibilityConverter.h">
+      <Filter>Converter</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
-    <Image Include="Assets\LockScreenLogo.scale-200.png" />
-    <Image Include="Assets\SplashScreen.scale-200.png" />
-    <Image Include="Assets\Square150x150Logo.scale-200.png" />
-    <Image Include="Assets\Square44x44Logo.scale-200.png" />
-    <Image Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
-    <Image Include="Assets\Wide310x150Logo.scale-200.png" />
     <Image Include="Assets\SmallTile.scale-100.png">
       <Filter>Assets</Filter>
     </Image>
@@ -155,18 +162,50 @@
     <Image Include="Assets\StoreLogo.scale-400.png">
       <Filter>Assets</Filter>
     </Image>
+    <Image Include="Assets\SplashScreen.scale-200.png">
+      <Filter>Assets</Filter>
+    </Image>
+    <Image Include="Assets\Square44x44Logo.scale-200.png">
+      <Filter>Assets</Filter>
+    </Image>
+    <Image Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png">
+      <Filter>Assets</Filter>
+    </Image>
+    <Image Include="Assets\Square150x150Logo.scale-200.png">
+      <Filter>Assets</Filter>
+    </Image>
+    <Image Include="Assets\Wide310x150Logo.scale-200.png">
+      <Filter>Assets</Filter>
+    </Image>
+    <Image Include="Assets\LockScreenLogo.scale-200.png">
+      <Filter>Assets</Filter>
+    </Image>
   </ItemGroup>
   <ItemGroup>
     <AppxManifest Include="Package.appxmanifest" />
   </ItemGroup>
   <ItemGroup>
-    <CopyFileToFolders Include="Config\default.conf" />
-    <CopyFileToFolders Include="Config\default.json" />
+    <CopyFileToFolders Include="Config\default.conf">
+      <Filter>Config</Filter>
+    </CopyFileToFolders>
+    <CopyFileToFolders Include="Config\default.json">
+      <Filter>Config</Filter>
+    </CopyFileToFolders>
+    <CopyFileToFolders Include="Config\minimal.conf">
+      <Filter>Config</Filter>
+    </CopyFileToFolders>
   </ItemGroup>
   <ItemGroup>
-    <Midl Include="Converter\DateTimeConverter.idl" />
-    <Midl Include="Model\ConfigViewModel.idl" />
     <Midl Include="Model\Netif.idl" />
+    <Midl Include="Model\ConfigViewModel.idl">
+      <Filter>Model</Filter>
+    </Midl>
+    <Midl Include="Converter\DateTimeConverter.idl">
+      <Filter>Converter</Filter>
+    </Midl>
+    <Midl Include="Converter\BoolToVisibilityConverter.idl">
+      <Filter>Converter</Filter>
+    </Midl>
   </ItemGroup>
   <ItemGroup>
     <None Include="PropertySheet.props" />
@@ -174,16 +213,48 @@
     <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup>
-    <Page Include="EditPage.xaml" />
-    <Page Include="MainPage.xaml" />
-    <Page Include="MmdbPage.xaml" />
-    <Page Include="DatPage.xaml" />
-    <Page Include="CertPage.xaml" />
-    <Page Include="MonacoEditPage.xaml" />
+    <Page Include="CertPage.xaml">
+      <Filter>Page</Filter>
+    </Page>
+    <Page Include="DatPage.xaml">
+      <Filter>Page</Filter>
+    </Page>
+    <Page Include="EditPage.xaml">
+      <Filter>Page</Filter>
+    </Page>
+    <Page Include="MmdbPage.xaml">
+      <Filter>Page</Filter>
+    </Page>
+    <Page Include="MonacoEditPage.xaml">
+      <Filter>Page</Filter>
+    </Page>
+    <Page Include="MainPage.xaml">
+      <Filter>Page</Filter>
+    </Page>
   </ItemGroup>
   <ItemGroup>
     <Filter Include="Assets">
       <UniqueIdentifier>{8bef39be-8f66-4942-b9f1-92668c496b3c}</UniqueIdentifier>
     </Filter>
+    <Filter Include="Converter">
+      <UniqueIdentifier>{9901d784-330d-41fc-9ca3-1e4570de2629}</UniqueIdentifier>
+    </Filter>
+    <Filter Include="Page">
+      <UniqueIdentifier>{68ac0a8f-3a19-4933-8ddd-f3de3c1af13b}</UniqueIdentifier>
+    </Filter>
+    <Filter Include="Util">
+      <UniqueIdentifier>{413bbf77-d776-44ef-869d-5d919f57eb25}</UniqueIdentifier>
+    </Filter>
+    <Filter Include="Config">
+      <UniqueIdentifier>{284a884f-cf86-45e3-abbc-d4d06635f76e}</UniqueIdentifier>
+    </Filter>
+    <Filter Include="Model">
+      <UniqueIdentifier>{36d194de-8c3a-47d3-a66b-a956b8316b80}</UniqueIdentifier>
+    </Filter>
+  </ItemGroup>
+  <ItemGroup>
+    <ApplicationDefinition Include="App.xaml">
+      <Filter>Page</Filter>
+    </ApplicationDefinition>
   </ItemGroup>
 </Project>

+ 77 - 12
Maple.App/MonacoEditPage.cpp

@@ -4,17 +4,18 @@
 #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"
+#include "ConfigUtil.h"
 
 using namespace winrt;
 using namespace Windows::Storage;
 using namespace Windows::Storage::Streams;
 using namespace Windows::UI::Xaml;
+using namespace Microsoft::Web::WebView2::Core;
 
 namespace winrt::Maple_App::implementation
 {
@@ -43,15 +44,58 @@ namespace winrt::Maple_App::implementation
         }
         m_webviewState = MonacoEditPageWebViewState::AwaitingEditorReady;
         co_await webview.EnsureCoreWebView2Async();
+        if (!m_webviewResourceRequestedEventHandle)
+        {
+            webview.CoreWebView2().AddWebResourceRequestedFilter(
+                hstring(CONFIG_ROOT_VIRTUAL_HOSTW) + L"*",
+                CoreWebView2WebResourceContext::Fetch
+            );
+            webview.CoreWebView2().AddWebResourceRequestedFilter(
+                hstring(CONFIG_ROOT_VIRTUAL_HOSTW) + L"*",
+                CoreWebView2WebResourceContext::XmlHttpRequest
+            );
+            webview.CoreWebView2().AddWebResourceRequestedFilter(
+                hstring(CONFIG_ROOT_VIRTUAL_HOSTW) + L"*",
+                CoreWebView2WebResourceContext::Document
+            );
+            m_webviewResourceRequestedEventHandle = webview.CoreWebView2().WebResourceRequested(
+                [weak = weak_ref(lifetime)](auto const webview, CoreWebView2WebResourceRequestedEventArgs const e) -> fire_and_forget
+                {
+                    auto const deferral = e.GetDeferral();
+            try {
+                if (auto const self{ weak.get() }) {
+                    auto const configDir = co_await ConfigUtil::GetConfigFolder();
+                    if (configDir.Path() != self->m_lastConfigFolderPath)
+                    {
+                        co_return;
+                    }
+                    auto path = to_string(e.Request().Uri());
+                    if (auto const pos{ path.find(CONFIG_ROOT_VIRTUAL_HOST) }; pos != std::string::npos)
+                    {
+                        path = path.replace(pos, strlen(CONFIG_ROOT_VIRTUAL_HOST), "");
+                    }
+
+                    auto const file = co_await configDir.GetFileAsync(to_hstring(path));
+                    auto const fstream = co_await file.OpenReadAsync();
+                    e.Response(webview.Environment().CreateWebResourceResponse(
+                        std::move(fstream),
+                        200,
+                        L"OK",
+                        hstring(L"Content-Length: ") + to_hstring(fstream.Size()) + L"\nContent-Type: text/plain\nAccess-Control-Allow-Origin: http://maple-monaco-editor-app-root.com"
+                    ));
+                }
+            }
+            catch (...)
+            {
+                UI::NotifyException(L"Handling web requests");
+            }
+            deferral.Complete();
+                });
+        }
         webview.CoreWebView2().SetVirtualHostNameToFolderMapping(
             L"maple-monaco-editor-app-root.com",
             packagePath,
-            Microsoft::Web::WebView2::Core::CoreWebView2HostResourceAccessKind::DenyCors
-        );
-        webview.CoreWebView2().SetVirtualHostNameToFolderMapping(
-            L"maple-monaco-editor-config-root.com",
-            configPath,
-            Microsoft::Web::WebView2::Core::CoreWebView2HostResourceAccessKind::Allow
+            CoreWebView2HostResourceAccessKind::DenyCors
         );
         webview.Source(Uri{ WEBVIEW_EDITOR_URL });
     }
@@ -70,11 +114,24 @@ namespace winrt::Maple_App::implementation
             {
                 co_return;
             }
+            auto const parent = co_await param.File().GetParentAsync();
+            if (!parent)
+            {
+                UI::NotifyUser("Failed to load config folder.", L"Error: loading file");
+                co_return;
+            }
+            auto const parentPath = parent.Path();
+            if (parentPath != m_lastConfigFolderPath)
+            {
+                m_lastConfigFolderPath = parentPath;
+                m_webviewState = MonacoEditPageWebViewState::Uninitialized;
+            }
             m_currentFileName = fileName;
             switch (m_webviewState)
             {
             case MonacoEditPageWebViewState::Uninitialized:
                 co_await initializeWebView();
+                co_await lifetime->WebView().ExecuteScriptAsync(L"window.mapleHostApi.resetFiles()");
                 break;
             case MonacoEditPageWebViewState::AwaitingEditorReady:
                 break;
@@ -149,6 +206,10 @@ namespace winrt::Maple_App::implementation
             }
             else if (cmd == "save")
             {
+                if (m_webviewState != MonacoEditPageWebViewState::EditorReady)
+                {
+                    co_return;
+                }
                 std::string path{ doc["path"] };
                 if (path == m_currentSavingFileName)
                 {
@@ -156,15 +217,19 @@ namespace winrt::Maple_App::implementation
                 }
                 m_currentSavingFileName = path;
 
-                auto const configDir{ co_await ApplicationData::Current().LocalFolder().CreateFolderAsync(L"config", CreationCollisionOption::OpenIfExists) };
-                auto const configDirPath{ to_string(configDir.Path()) + "\\" };
+                auto const configDir = co_await ConfigUtil::GetConfigFolder();
+                if (configDir.Path() != m_lastConfigFolderPath)
+                {
+                    co_return;
+                }
                 if (auto const pos{ path.find(CONFIG_ROOT_VIRTUAL_HOST) }; pos != std::string::npos)
                 {
-                    path = path.replace(pos, strlen(CONFIG_ROOT_VIRTUAL_HOST), configDirPath);
+                    path = path.replace(pos, strlen(CONFIG_ROOT_VIRTUAL_HOST), "");
                 }
+                hstring fileName = to_hstring(path);
                 StorageFile file{ nullptr };
                 try {
-                    file = co_await StorageFile::GetFileFromPathAsync(to_hstring(path));
+                    file = co_await configDir.GetFileAsync(fileName);
                 }
                 catch (hresult_error const& hr)
                 {
@@ -176,13 +241,13 @@ namespace winrt::Maple_App::implementation
                 }
                 std::string const data{ doc["text"] };
                 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>>();
+                        fstream.Size(data.size());
                         wr.Close();
                     }
                     catch (...)

+ 2 - 1
Maple.App/MonacoEditPage.h

@@ -32,14 +32,15 @@ namespace winrt::Maple_App::implementation
         inline static std::function<IAsyncAction()> SaveModifiedContent{ nullptr };
     private:
         auto const static inline packagePath = Windows::ApplicationModel::Package::Current().InstalledPath();
-        auto const static inline configPath = Windows::Storage::ApplicationData::Current().LocalFolder().Path() + L"\\config";
 
         IAsyncAction initializeWebView();
 
+        event_token m_webviewResourceRequestedEventHandle{};
         MonacoEditPageWebViewState m_webviewState{ MonacoEditPageWebViewState::Uninitialized };
         std::coroutine_handle<> m_fileSaveHandle{ nullptr };
         hstring m_currentFileName{};
         std::string m_currentSavingFileName{};
+        hstring m_lastConfigFolderPath{};
     };
 }
 

+ 11 - 1
Maple.App/MonacoEditor/src/init.ts

@@ -15,6 +15,7 @@ declare global {
         mapleHostApi: {
             loadFile: (url: string) => void
             requestSaveCurrent: () => void
+            resetFiles: () => void
         }
         MonacoEnvironment?: monaco.Environment | undefined,
         chrome: {
@@ -120,7 +121,16 @@ function initLang() {
         }
         saveFile(current.url, current.text)
     }
-    window.mapleHostApi = { loadFile, requestSaveCurrent }
+    async function resetFiles() {
+        await currentFileLock.withLock((_currentFile, setCurrentFile) => {
+            setCurrentFile('')
+            editor.setModel(null)
+            monaco.editor.getModels().forEach(m => m.dispose())
+            editorStates.clear()
+        })
+        reportEditorReady()
+    }
+    window.mapleHostApi = { loadFile, requestSaveCurrent, resetFiles }
 
     window.addEventListener('focusout', () => {
         requestSaveCurrent()