mirror of
https://github.com/Blinue/Magpie.git
synced 2026-06-24 02:04:10 +00:00
refactor: 重新架构
This commit is contained in:
parent
f1c909c4f7
commit
4bc871fbee
8 changed files with 199 additions and 300 deletions
|
|
@ -149,7 +149,7 @@
|
|||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="XamlHost.h" />
|
||||
<ClInclude Include="XamlApp.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
|
@ -162,7 +162,7 @@
|
|||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="main.cpp" />
|
||||
<ClCompile Include="XamlHost.cpp" />
|
||||
<ClCompile Include="XamlApp.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
<ClInclude Include="pch.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="XamlHost.h">
|
||||
<ClInclude Include="XamlApp.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
<ClCompile Include="pch.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="XamlHost.cpp">
|
||||
<ClCompile Include="XamlApp.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
|
|
|
|||
145
src/Magpie/XamlApp.cpp
Normal file
145
src/Magpie/XamlApp.cpp
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
#include "pch.h"
|
||||
#include "XamlApp.h"
|
||||
#include <winrt/Windows.UI.Core.h>
|
||||
#include <CoreWindow.h>
|
||||
|
||||
|
||||
ATOM RegisterWndClass(
|
||||
HINSTANCE hInstance,
|
||||
LRESULT (*wndProc)(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam),
|
||||
const wchar_t* className
|
||||
) {
|
||||
WNDCLASSEXW wcex{};
|
||||
|
||||
wcex.cbSize = sizeof(WNDCLASSEX);
|
||||
wcex.lpfnWndProc = wndProc;
|
||||
wcex.cbClsExtra = 0;
|
||||
wcex.cbWndExtra = 0;
|
||||
wcex.hInstance = hInstance;
|
||||
wcex.hIcon = NULL;
|
||||
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
|
||||
wcex.lpszMenuName = NULL;
|
||||
wcex.lpszClassName = className;
|
||||
wcex.hIconSm = NULL;
|
||||
|
||||
return RegisterClassEx(&wcex);
|
||||
}
|
||||
|
||||
|
||||
RTL_OSVERSIONINFOW GetOSVersion() {
|
||||
HMODULE hNtDll = GetModuleHandle(L"ntdll.dll");
|
||||
if (!hNtDll) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto rtlGetVersion = (LONG(WINAPI*)(PRTL_OSVERSIONINFOW))GetProcAddress(hNtDll, "RtlGetVersion");
|
||||
if (rtlGetVersion == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
RTL_OSVERSIONINFOW version{};
|
||||
version.dwOSVersionInfoSize = sizeof(version);
|
||||
rtlGetVersion(&version);
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
bool XamlApp::Initialize(HINSTANCE hInstance, const wchar_t* className, const wchar_t* title) {
|
||||
RegisterWndClass(hInstance, _WndProcStatic, className);
|
||||
|
||||
auto osVersion = GetOSVersion();
|
||||
bool isWin11 = osVersion.dwMajorVersion == 10 && osVersion.dwMinorVersion == 0 && osVersion.dwBuildNumber >= 22000;
|
||||
|
||||
_hwndXamlHost = CreateWindowEx(isWin11 ? WS_EX_NOREDIRECTIONBITMAP | WS_EX_DLGMODALFRAME : 0,
|
||||
className, isWin11 ? L"" : title, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 1000, 700,
|
||||
nullptr, nullptr, hInstance, nullptr);
|
||||
|
||||
if (!_hwndXamlHost) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isWin11) {
|
||||
constexpr const DWORD DWMWA_MICA_EFFECT = 1029;
|
||||
|
||||
BOOL value = TRUE;
|
||||
DwmSetWindowAttribute(_hwndXamlHost, DWMWA_MICA_EFFECT, &value, sizeof(value));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void XamlApp::Show(winrt::Windows::UI::Xaml::UIElement xamlElement) {
|
||||
_xamlElement = xamlElement;
|
||||
|
||||
// 在 Win10 上可能导致任务栏出现空的 DesktopWindowXamlSource 窗口
|
||||
// 见 https://github.com/microsoft/microsoft-ui-xaml/issues/6490
|
||||
ShowWindow(_hwndXamlHost, SW_SHOW);
|
||||
|
||||
_xamlSource = winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource();
|
||||
_xamlSourceNative2 = _xamlSource.as<IDesktopWindowXamlSourceNative2>();
|
||||
|
||||
auto interop = _xamlSource.as<IDesktopWindowXamlSourceNative>();
|
||||
interop->AttachToWindow(_hwndXamlHost);
|
||||
|
||||
interop->get_WindowHandle(&_hwndXamlIsland);
|
||||
_xamlSource.Content(xamlElement);
|
||||
|
||||
_OnResize();
|
||||
|
||||
// 防止关闭时出现 DesktopWindowXamlSource 窗口
|
||||
auto coreWindow = winrt::Windows::UI::Core::CoreWindow::GetForCurrentThread();
|
||||
if (coreWindow) {
|
||||
HWND hwndDWXS;
|
||||
coreWindow.as<ICoreWindowInterop>()->get_WindowHandle(&hwndDWXS);
|
||||
ShowWindow(hwndDWXS, SW_HIDE);
|
||||
}
|
||||
}
|
||||
|
||||
int XamlApp::Run() {
|
||||
MSG msg;
|
||||
|
||||
// 主消息循环
|
||||
while (GetMessage(&msg, nullptr, 0, 0)) {
|
||||
if (_xamlSource) {
|
||||
BOOL processed = FALSE;
|
||||
HRESULT hr = _xamlSourceNative2->PreTranslateMessage(&msg, &processed);
|
||||
if (SUCCEEDED(hr) && processed) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
return (int)msg.wParam;
|
||||
}
|
||||
|
||||
void XamlApp::_OnResize() {
|
||||
RECT clientRect;
|
||||
GetClientRect(_hwndXamlHost, &clientRect);
|
||||
SetWindowPos(_hwndXamlIsland, NULL, 0, 0, clientRect.right - clientRect.left, clientRect.bottom - clientRect.top, SWP_SHOWWINDOW);
|
||||
}
|
||||
|
||||
LRESULT XamlApp::_WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||
switch (message) {
|
||||
case WM_SIZE:
|
||||
_OnResize();
|
||||
return 0;
|
||||
case WM_SETFOCUS:
|
||||
if (_hwndXamlIsland) {
|
||||
SetFocus(_hwndXamlIsland);
|
||||
}
|
||||
return 0;
|
||||
case WM_DESTROY:
|
||||
_xamlSourceNative2 = nullptr;
|
||||
_xamlSource.Close();
|
||||
_xamlSource = nullptr;
|
||||
_xamlElement = nullptr;
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||
}
|
||||
38
src/Magpie/XamlApp.h
Normal file
38
src/Magpie/XamlApp.h
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
#include "pch.h"
|
||||
#include <windows.ui.xaml.hosting.desktopwindowxamlsource.h>
|
||||
|
||||
|
||||
class XamlApp {
|
||||
public:
|
||||
XamlApp() = default;
|
||||
|
||||
XamlApp(const XamlApp&) = delete;
|
||||
XamlApp(XamlApp&&) = delete;
|
||||
|
||||
static XamlApp& Get() {
|
||||
static XamlApp instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
bool Initialize(HINSTANCE hInstance, const wchar_t* className, const wchar_t* title);
|
||||
|
||||
void Show(winrt::Windows::UI::Xaml::UIElement xamlElement);
|
||||
|
||||
int Run();
|
||||
|
||||
private:
|
||||
void _OnResize();
|
||||
|
||||
static LRESULT _WndProcStatic(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
return Get()._WndProc(hWnd, msg, wParam, lParam);
|
||||
}
|
||||
LRESULT _WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||
|
||||
winrt::Windows::UI::Xaml::UIElement _xamlElement{ nullptr };
|
||||
HWND _hwndXamlHost = NULL;
|
||||
HWND _hwndXamlIsland = NULL;
|
||||
|
||||
winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _xamlSource{ nullptr };
|
||||
winrt::com_ptr<IDesktopWindowXamlSourceNative2> _xamlSourceNative2;
|
||||
};
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
#include "pch.h"
|
||||
#include "XamlHost.h"
|
||||
#include <winrt/Windows.UI.Core.h>
|
||||
#include <CoreWindow.h>
|
||||
|
||||
|
||||
namespace winrt {
|
||||
using namespace Windows::UI::Xaml::Hosting;
|
||||
}
|
||||
|
||||
|
||||
void XamlHost::Attach(HWND parent, winrt::Windows::UI::Xaml::UIElement xamlElement) {
|
||||
if (HasAttach()) {
|
||||
Detach();
|
||||
}
|
||||
_xamlSource = winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource();
|
||||
_xamlSourceNative2 = _xamlSource.as<IDesktopWindowXamlSourceNative2>();
|
||||
|
||||
auto interop = _xamlSource.as<IDesktopWindowXamlSourceNative>();
|
||||
interop->AttachToWindow(parent);
|
||||
_hwndParent = parent;
|
||||
|
||||
interop->get_WindowHandle(&_hwndXamlIsland);
|
||||
_xamlSource.Content(xamlElement);
|
||||
|
||||
_OnResize();
|
||||
|
||||
// 防止关闭时出现 DesktopWindowXamlSource 窗口
|
||||
auto coreWindow = winrt::Windows::UI::Core::CoreWindow::GetForCurrentThread();
|
||||
if (coreWindow) {
|
||||
HWND hwndDWXS;
|
||||
coreWindow.as<ICoreWindowInterop>()->get_WindowHandle(&hwndDWXS);
|
||||
ShowWindow(hwndDWXS, SW_HIDE);
|
||||
}
|
||||
}
|
||||
|
||||
void XamlHost::Detach() {
|
||||
if (!HasAttach()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_xamlSource.Close();
|
||||
_xamlSource = nullptr;
|
||||
}
|
||||
|
||||
bool XamlHost::PreHandleMessage(const MSG& msg) {
|
||||
if (!HasAttach()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOL processed = FALSE;
|
||||
HRESULT hr = _xamlSourceNative2->PreTranslateMessage(&msg, &processed);
|
||||
if (FAILED(hr)) {
|
||||
processed = FALSE;
|
||||
}
|
||||
|
||||
if (processed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void XamlHost::HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||
if (!HasAttach()) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message) {
|
||||
case WM_SIZE:
|
||||
_OnResize();
|
||||
break;
|
||||
case WM_SETFOCUS:
|
||||
if (_hwndXamlIsland) {
|
||||
SetFocus(_hwndXamlIsland);
|
||||
}
|
||||
break;
|
||||
case WM_DESTROY:
|
||||
Detach();
|
||||
break;
|
||||
case WM_PAINT:
|
||||
{
|
||||
PAINTSTRUCT ps;
|
||||
HDC hdc = BeginPaint(hWnd, &ps);
|
||||
EndPaint(hWnd, &ps);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void XamlHost::_OnResize() {
|
||||
if (!HasAttach()) {
|
||||
return;
|
||||
}
|
||||
|
||||
RECT clientRect;
|
||||
GetClientRect(_hwndParent, &clientRect);
|
||||
SetWindowPos(_hwndXamlIsland, NULL, 0, 0, clientRect.right - clientRect.left, clientRect.bottom - clientRect.top, SWP_SHOWWINDOW);
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
#pragma once
|
||||
#include "pch.h"
|
||||
#include <windows.ui.xaml.hosting.desktopwindowxamlsource.h>
|
||||
|
||||
|
||||
class XamlHost {
|
||||
public:
|
||||
XamlHost() = default;
|
||||
|
||||
XamlHost(const XamlHost&) = delete;
|
||||
XamlHost(XamlHost&&) = delete;
|
||||
|
||||
~XamlHost() {
|
||||
Detach();
|
||||
}
|
||||
|
||||
bool HasAttach() const {
|
||||
return !!_xamlSource;
|
||||
}
|
||||
|
||||
void Attach(HWND parent, winrt::Windows::UI::Xaml::UIElement xamlElement);
|
||||
|
||||
void Detach();
|
||||
|
||||
bool PreHandleMessage(const MSG& msg);
|
||||
|
||||
void HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||
|
||||
private:
|
||||
void _OnResize();
|
||||
|
||||
winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _xamlSource{ nullptr };
|
||||
winrt::com_ptr<IDesktopWindowXamlSourceNative2> _xamlSourceNative2;
|
||||
|
||||
HWND _hwndXamlIsland = NULL;
|
||||
HWND _hwndParent = NULL;
|
||||
};
|
||||
|
|
@ -2,157 +2,22 @@
|
|||
//
|
||||
|
||||
#include "pch.h"
|
||||
#include "XamlHost.h"
|
||||
#include "XamlApp.h"
|
||||
|
||||
|
||||
// 全局变量:
|
||||
HINSTANCE hInst; // 当前实例
|
||||
const wchar_t* szTitle = L"Magpie"; // 标题栏文本
|
||||
const wchar_t* szWindowClass = L"Magpie_XamlHost"; // 主窗口类名
|
||||
winrt::Magpie::App::App hostApp{ nullptr };
|
||||
winrt::Magpie::App::MainPage _myUserControl{ nullptr };
|
||||
XamlHost xamlHost;
|
||||
|
||||
|
||||
// 此代码模块中包含的函数的前向声明:
|
||||
ATOM MyRegisterClass(HINSTANCE hInstance);
|
||||
BOOL InitInstance(HINSTANCE, int);
|
||||
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
|
||||
_In_opt_ HINSTANCE hPrevInstance,
|
||||
_In_ LPWSTR lpCmdLine,
|
||||
_In_ int nCmdShow) {
|
||||
UNREFERENCED_PARAMETER(hPrevInstance);
|
||||
UNREFERENCED_PARAMETER(lpCmdLine);
|
||||
|
||||
int APIENTRY wWinMain(
|
||||
_In_ HINSTANCE hInstance,
|
||||
_In_opt_ HINSTANCE hPrevInstance,
|
||||
_In_ LPWSTR lpCmdLine,
|
||||
_In_ int nCmdShow
|
||||
) {
|
||||
winrt::init_apartment(winrt::apartment_type::single_threaded);
|
||||
|
||||
MyRegisterClass(hInstance);
|
||||
auto& app = XamlApp::Get();
|
||||
app.Initialize(hInstance, L"Magpie_XamlHost", L"Magpie");
|
||||
|
||||
// 执行应用程序初始化:
|
||||
if (!InitInstance(hInstance, nCmdShow)) {
|
||||
return FALSE;
|
||||
}
|
||||
winrt::Magpie::App::App hostApp = winrt::Magpie::App::App{};
|
||||
app.Show(winrt::Magpie::App::MainPage());
|
||||
|
||||
MSG msg;
|
||||
|
||||
// 主消息循环:
|
||||
while (GetMessage(&msg, nullptr, 0, 0)) {
|
||||
if (xamlHost.PreHandleMessage(msg)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
return (int)msg.wParam;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// 函数: MyRegisterClass()
|
||||
//
|
||||
// 目标: 注册窗口类。
|
||||
//
|
||||
ATOM MyRegisterClass(HINSTANCE hInstance)
|
||||
{
|
||||
WNDCLASSEXW wcex{};
|
||||
|
||||
wcex.cbSize = sizeof(WNDCLASSEX);
|
||||
wcex.lpfnWndProc = WndProc;
|
||||
wcex.cbClsExtra = 0;
|
||||
wcex.cbWndExtra = 0;
|
||||
wcex.hInstance = hInstance;
|
||||
wcex.hIcon = NULL;
|
||||
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
|
||||
wcex.lpszMenuName = NULL;
|
||||
wcex.lpszClassName = szWindowClass;
|
||||
wcex.hIconSm = NULL;
|
||||
|
||||
return RegisterClassEx(&wcex);
|
||||
}
|
||||
|
||||
RTL_OSVERSIONINFOW GetOSVersion() {
|
||||
HMODULE hNtDll = GetModuleHandle(L"ntdll.dll");
|
||||
if (!hNtDll) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto rtlGetVersion = (LONG(WINAPI*)(PRTL_OSVERSIONINFOW))GetProcAddress(hNtDll, "RtlGetVersion");
|
||||
if (rtlGetVersion == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
RTL_OSVERSIONINFOW version{};
|
||||
version.dwOSVersionInfoSize = sizeof(version);
|
||||
rtlGetVersion(&version);
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
//
|
||||
// 函数: InitInstance(HINSTANCE, int)
|
||||
//
|
||||
// 目标: 保存实例句柄并创建主窗口
|
||||
//
|
||||
// 注释:
|
||||
//
|
||||
// 在此函数中,我们在全局变量中保存实例句柄并
|
||||
// 创建和显示主程序窗口。
|
||||
//
|
||||
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) {
|
||||
hInst = hInstance; // Store instance handle in our global variable
|
||||
|
||||
auto osVersion = GetOSVersion();
|
||||
bool isWin11 = osVersion.dwMajorVersion == 10 && osVersion.dwMinorVersion == 0 && osVersion.dwBuildNumber >= 22000;
|
||||
|
||||
HWND hWnd = CreateWindowEx(isWin11 ? WS_EX_NOREDIRECTIONBITMAP | WS_EX_DLGMODALFRAME : 0, szWindowClass, isWin11 ? L"" : szTitle, WS_OVERLAPPEDWINDOW,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT, 1000, 700, nullptr, nullptr, hInstance, nullptr);
|
||||
|
||||
if (!hWnd) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (isWin11) {
|
||||
constexpr const DWORD DWMWA_MICA_EFFECT = 1029;
|
||||
|
||||
BOOL value = TRUE;
|
||||
DwmSetWindowAttribute(hWnd, DWMWA_MICA_EFFECT, &value, sizeof(value));
|
||||
}
|
||||
|
||||
// 在 Win10 上可能导致任务栏出现空的 DesktopWindowXamlSource 窗口
|
||||
// 见 https://github.com/microsoft/microsoft-ui-xaml/issues/6490
|
||||
hostApp = winrt::Magpie::App::App{};
|
||||
ShowWindow(hWnd, nCmdShow);
|
||||
|
||||
xamlHost.Attach(hWnd, winrt::Magpie::App::MainPage());
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
//
|
||||
// 函数: WndProc(HWND, UINT, WPARAM, LPARAM)
|
||||
//
|
||||
// 目标: 处理主窗口的消息。
|
||||
//
|
||||
// WM_COMMAND - 处理应用程序菜单
|
||||
// WM_PAINT - 绘制主窗口
|
||||
// WM_DESTROY - 发送退出消息并返回
|
||||
//
|
||||
//
|
||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||
xamlHost.HandleMessage(hWnd, message, wParam, lParam);
|
||||
|
||||
switch (message) {
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
break;
|
||||
default:
|
||||
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
return 0;
|
||||
return app.Run();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,17 +38,10 @@
|
|||
#define NOMCX
|
||||
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
#include <dwmapi.h>
|
||||
|
||||
// C++ 运行时头文件
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <algorithm>
|
||||
#include <string_view>
|
||||
#include <span>
|
||||
|
||||
// C++/WinRT 头文件
|
||||
#include <winrt/base.h>
|
||||
|
|
@ -61,7 +54,3 @@
|
|||
|
||||
|
||||
#pragma comment(lib, "dwmapi.lib")
|
||||
|
||||
|
||||
using namespace std::literals::string_literals;
|
||||
using namespace std::literals::string_view_literals;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue