mirror of
https://github.com/ItsDrike/hyprland-dwindle-autogroup.git
synced 2024-12-25 13:14:35 +00:00
Initial commit
This commit is contained in:
commit
2d883200bc
14
.clang-format
Normal file
14
.clang-format
Normal file
|
@ -0,0 +1,14 @@
|
|||
BasedOnStyle: LLVM
|
||||
BreakBeforeBraces: Stroustrup
|
||||
TabWidth: 4
|
||||
IndentWidth: 4
|
||||
UseTab: Never
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
IndentCaseLabels: true
|
||||
ColumnLimit: 200
|
||||
PointerAlignment: Left
|
||||
AllowShortBlocksOnASingleLine: Empty
|
||||
AllowShortEnumsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: Empty
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.so
|
||||
compile_flags.txt
|
28
Makefile
Normal file
28
Makefile
Normal file
|
@ -0,0 +1,28 @@
|
|||
# compile with HYPRLAND_HEADERS=<path_to_hl> make all
|
||||
# make sure that the path above is to the root hl repo directory, NOT src/
|
||||
# and that you have ran `make protocols` in the hl dir.
|
||||
|
||||
PLUGIN_NAME=dwindle-autogroup
|
||||
|
||||
SOURCE_FILES=$(wildcard src/*.cpp)
|
||||
|
||||
.PHONY: clean clangd
|
||||
|
||||
all: check_env $(PLUGIN_NAME).so
|
||||
|
||||
install: all
|
||||
cp $(PLUGIN_NAME).so ${HOME}/.local/share/hyprload/plugins/bin
|
||||
|
||||
check_env:
|
||||
ifndef HYPRLAND_HEADERS
|
||||
$(error HYPRLAND_HEADERS is undefined! Please set it to the path to the root of the configured Hyprland repo)
|
||||
endif
|
||||
|
||||
$(PLUGIN_NAME).so: $(SOURCE_FILES) $(INCLUDE_FILES)
|
||||
g++ -shared -fPIC --no-gnu-unique $(SOURCE_FILES) -o $(PLUGIN_NAME).so -g -I "/usr/include/pixman-1" -I "/usr/include/libdrm" -I "${HYPRLAND_HEADERS}" -Iinclude -std=c++23
|
||||
|
||||
clean:
|
||||
rm ./${PLUGIN_NAME}.so
|
||||
|
||||
clangd:
|
||||
printf "%b" "-I/usr/include/pixman-1\n-I/usr/include/libdrm\n-I${HYPRLAND_HEADERS}\n-Iinclude\n-std=c++2b" > compile_flags.txt
|
34
README.md
Normal file
34
README.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Dwindle-Autogroup
|
||||
|
||||
This plugin changes the behavior of the `togglegroup` dispatcher for dwindle layout, to automatically group all of the child windows when a new group is created.
|
||||
|
||||
Before Hyprland `v0.23.0beta`, this was actually the default behavior, however as that release introduced group support for other layouts, including floating windows, this dwindle specific feature was removed and `togglegroup` now only creates a group window, and requires you to move in all of the windows that should be a part of that group into it manually.
|
||||
|
||||
## Installation
|
||||
|
||||
Since Hyprland plugins don't have ABI guarantees, you should download the Hyprland source and compile it if you plan to use plugins. This ensures the compiler version is the same between the Hyprland build you're running, and the plugins you are using.
|
||||
|
||||
The guide on compiling and installing Hyprland manually is on the [wiki](http://wiki.hyprland.org/Getting-Started/Installation/#manual-manual-build)
|
||||
|
||||
## Using [hyprload](https://github.com/Duckonaut/hyprload)
|
||||
1. Export the `HYPRLAND_HEADERS` variable to point to the root directory of the Hyprland repo
|
||||
- `export HYPRLAND_HEADERS="$HOME/repos/Hyprland"`
|
||||
2. Install
|
||||
- `make install`
|
||||
|
||||
## Manual installation
|
||||
1. Export the `HYPRLAND_HEADERS` variable to point to the root directory of the Hyprland repo
|
||||
- `export HYPRLAND_HEADERS="$HOME/repos/Hyprland"`
|
||||
2. Compile
|
||||
- `make all`
|
||||
3. Add this line to the bottom of your hyprland config
|
||||
- `exec-once=hyprctl plugin load <ABSOLUTE PATH TO split-monitor-workspaces.so>`
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
When developing, it is useful to run `make clangd`, to generate `compile_flags.txt` file, allowing Clang language server to properly recognize the imports, and give you autocompletion.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
I'm very new to C++ development, and I'm also not very familiar with Hyprland's codebase. So while I will do my best to follow best practices, with so little experience, you can be pretty much certain that there will be bugs, and that the code will not be pretty. But hey, if you know about something that I did wrong, feel free to PR/make an issue about it.
|
10
hyprload.toml
Normal file
10
hyprload.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[dwindle-autogroup]
|
||||
description = "Dwindle Autogroup"
|
||||
version = "1.0.0"
|
||||
author = "ItsDrike"
|
||||
|
||||
[diwndle-autogroup.build]
|
||||
output = "dwindle-autogroup.so"
|
||||
steps = [
|
||||
"make all",
|
||||
]
|
29
include/customDecoration.hpp
Normal file
29
include/customDecoration.hpp
Normal file
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#define WLR_USE_UNSTABLE
|
||||
|
||||
#include <src/render/decorations/IHyprWindowDecoration.hpp>
|
||||
|
||||
class CCustomDecoration : public IHyprWindowDecoration {
|
||||
public:
|
||||
CCustomDecoration(CWindow*);
|
||||
virtual ~CCustomDecoration();
|
||||
|
||||
virtual SWindowDecorationExtents getWindowDecorationExtents();
|
||||
|
||||
virtual void draw(CMonitor*, float a, const Vector2D& offset);
|
||||
|
||||
virtual eDecorationType getDecorationType();
|
||||
|
||||
virtual void updateWindow(CWindow*);
|
||||
|
||||
virtual void damageEntire();
|
||||
|
||||
private:
|
||||
SWindowDecorationExtents m_seExtents;
|
||||
|
||||
CWindow* m_pWindow = nullptr;
|
||||
|
||||
Vector2D m_vLastWindowPos;
|
||||
Vector2D m_vLastWindowSize;
|
||||
};
|
32
include/customLayout.hpp
Normal file
32
include/customLayout.hpp
Normal file
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#define WLR_USE_UNSTABLE
|
||||
|
||||
#include <src/layout/IHyprLayout.hpp>
|
||||
|
||||
struct SWindowData {
|
||||
CWindow* pWindow = nullptr;
|
||||
};
|
||||
|
||||
class CHyprCustomLayout : public IHyprLayout {
|
||||
public:
|
||||
virtual void onWindowCreatedTiling(CWindow*);
|
||||
virtual void onWindowRemovedTiling(CWindow*);
|
||||
virtual bool isWindowTiled(CWindow*);
|
||||
virtual void recalculateMonitor(const int&);
|
||||
virtual void recalculateWindow(CWindow*);
|
||||
virtual void resizeActiveWindow(const Vector2D&, CWindow* pWindow = nullptr);
|
||||
virtual void fullscreenRequestForWindow(CWindow*, eFullscreenMode, bool);
|
||||
virtual std::any layoutMessage(SLayoutMessageHeader, std::string);
|
||||
virtual SWindowRenderLayoutHints requestRenderHints(CWindow*);
|
||||
virtual void switchWindows(CWindow*, CWindow*);
|
||||
virtual void alterSplitRatio(CWindow*, float, bool);
|
||||
virtual std::string getLayoutName();
|
||||
virtual void replaceWindowDataWith(CWindow* from, CWindow* to);
|
||||
|
||||
virtual void onEnable();
|
||||
virtual void onDisable();
|
||||
|
||||
private:
|
||||
std::vector<SWindowData> m_vWindowData;
|
||||
};
|
5
include/globals.hpp
Normal file
5
include/globals.hpp
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include <src/plugins/PluginAPI.hpp>
|
||||
|
||||
inline HANDLE PHANDLE = nullptr;
|
80
src/customDecoration.cpp
Normal file
80
src/customDecoration.cpp
Normal file
|
@ -0,0 +1,80 @@
|
|||
#include "customDecoration.hpp"
|
||||
#include "globals.hpp"
|
||||
#include <src/Compositor.hpp>
|
||||
#include <src/Window.hpp>
|
||||
|
||||
CCustomDecoration::CCustomDecoration(CWindow* pWindow)
|
||||
{
|
||||
m_pWindow = pWindow;
|
||||
m_vLastWindowPos = pWindow->m_vRealPosition.vec();
|
||||
m_vLastWindowSize = pWindow->m_vRealSize.vec();
|
||||
}
|
||||
|
||||
CCustomDecoration::~CCustomDecoration()
|
||||
{
|
||||
damageEntire();
|
||||
}
|
||||
|
||||
SWindowDecorationExtents CCustomDecoration::getWindowDecorationExtents()
|
||||
{
|
||||
return m_seExtents;
|
||||
}
|
||||
|
||||
void CCustomDecoration::draw(CMonitor* pMonitor, float a, const Vector2D& offset)
|
||||
{
|
||||
if (!g_pCompositor->windowValidMapped(m_pWindow))
|
||||
return;
|
||||
|
||||
if (!m_pWindow->m_sSpecialRenderData.decorate)
|
||||
return;
|
||||
|
||||
static auto* const PCOLOR = &HyprlandAPI::getConfigValue(PHANDLE, "plugin:example:border_color")->intValue;
|
||||
static auto* const PROUNDING = &HyprlandAPI::getConfigValue(PHANDLE, "decoration:rounding")->intValue;
|
||||
static auto* const PBORDERSIZE = &HyprlandAPI::getConfigValue(PHANDLE, "general:border_size")->intValue;
|
||||
|
||||
const auto ROUNDING =
|
||||
!m_pWindow->m_sSpecialRenderData.rounding ? 0 : (m_pWindow->m_sAdditionalConfigData.rounding.toUnderlying() == -1 ? *PROUNDING : m_pWindow->m_sAdditionalConfigData.rounding.toUnderlying());
|
||||
|
||||
// draw the border
|
||||
wlr_box fullBox = {(int)(m_vLastWindowPos.x - *PBORDERSIZE), (int)(m_vLastWindowPos.y - *PBORDERSIZE), (int)(m_vLastWindowSize.x + 2.0 * *PBORDERSIZE),
|
||||
(int)(m_vLastWindowSize.y + 2.0 * *PBORDERSIZE)};
|
||||
|
||||
fullBox.x -= pMonitor->vecPosition.x;
|
||||
fullBox.y -= pMonitor->vecPosition.y;
|
||||
|
||||
m_seExtents = {{m_vLastWindowPos.x - fullBox.x - pMonitor->vecPosition.x + 2, m_vLastWindowPos.y - fullBox.y - pMonitor->vecPosition.y + 2},
|
||||
{fullBox.x + fullBox.width + pMonitor->vecPosition.x - m_vLastWindowPos.x - m_vLastWindowSize.x + 2,
|
||||
fullBox.y + fullBox.height + pMonitor->vecPosition.y - m_vLastWindowPos.y - m_vLastWindowSize.y + 2}};
|
||||
|
||||
fullBox.x += offset.x;
|
||||
fullBox.y += offset.y;
|
||||
|
||||
if (fullBox.width < 1 || fullBox.height < 1)
|
||||
return; // don't draw invisible shadows
|
||||
|
||||
g_pHyprOpenGL->scissor((wlr_box*)nullptr);
|
||||
|
||||
scaleBox(&fullBox, pMonitor->scale);
|
||||
g_pHyprOpenGL->renderBorder(&fullBox, CColor(*PCOLOR), *PROUNDING * pMonitor->scale + *PBORDERSIZE * 2, a);
|
||||
}
|
||||
|
||||
eDecorationType CCustomDecoration::getDecorationType()
|
||||
{
|
||||
return DECORATION_CUSTOM;
|
||||
}
|
||||
|
||||
void CCustomDecoration::updateWindow(CWindow* pWindow)
|
||||
{
|
||||
|
||||
m_vLastWindowPos = pWindow->m_vRealPosition.vec();
|
||||
m_vLastWindowSize = pWindow->m_vRealSize.vec();
|
||||
|
||||
damageEntire();
|
||||
}
|
||||
|
||||
void CCustomDecoration::damageEntire()
|
||||
{
|
||||
wlr_box dm = {(int)(m_vLastWindowPos.x - m_seExtents.topLeft.x), (int)(m_vLastWindowPos.y - m_seExtents.topLeft.y), (int)(m_vLastWindowSize.x + m_seExtents.topLeft.x + m_seExtents.bottomRight.x),
|
||||
(int)m_seExtents.topLeft.y};
|
||||
g_pHyprRenderer->damageBox(&dm);
|
||||
}
|
96
src/customLayout.cpp
Normal file
96
src/customLayout.cpp
Normal file
|
@ -0,0 +1,96 @@
|
|||
#include "customLayout.hpp"
|
||||
#include "globals.hpp"
|
||||
#include <src/Compositor.hpp>
|
||||
|
||||
void CHyprCustomLayout::onWindowCreatedTiling(CWindow* pWindow)
|
||||
{
|
||||
const auto PMONITOR = g_pCompositor->getMonitorFromID(pWindow->m_iMonitorID);
|
||||
const auto SIZE = PMONITOR->vecSize;
|
||||
|
||||
// these are used for focus and move calculations, and are *required* to touch
|
||||
// for moving focus to work properly.
|
||||
pWindow->m_vPosition = Vector2D{(SIZE.x / 2.0) * (m_vWindowData.size() % 2), (SIZE.y / 2.0) * (int)(m_vWindowData.size() > 1)};
|
||||
pWindow->m_vSize = SIZE / 2.0;
|
||||
|
||||
// this is the actual pos and size of the window (where it's rendered)
|
||||
pWindow->m_vRealPosition = pWindow->m_vPosition + Vector2D{10, 10};
|
||||
pWindow->m_vRealSize = pWindow->m_vSize - Vector2D{20, 20};
|
||||
|
||||
const auto PDATA = &m_vWindowData.emplace_back();
|
||||
PDATA->pWindow = pWindow;
|
||||
}
|
||||
|
||||
void CHyprCustomLayout::onWindowRemovedTiling(CWindow* pWindow)
|
||||
{
|
||||
std::erase_if(m_vWindowData, [&](const auto& other) { return other.pWindow == pWindow; });
|
||||
}
|
||||
|
||||
bool CHyprCustomLayout::isWindowTiled(CWindow* pWindow)
|
||||
{
|
||||
return std::find_if(m_vWindowData.begin(), m_vWindowData.end(), [&](const auto& other) { return other.pWindow == pWindow; }) != m_vWindowData.end();
|
||||
}
|
||||
|
||||
void CHyprCustomLayout::recalculateMonitor(const int& eIdleInhibitMode)
|
||||
{
|
||||
; // empty
|
||||
}
|
||||
|
||||
void CHyprCustomLayout::recalculateWindow(CWindow* pWindow)
|
||||
{
|
||||
; // empty
|
||||
}
|
||||
|
||||
void CHyprCustomLayout::resizeActiveWindow(const Vector2D& delta, CWindow* pWindow)
|
||||
{
|
||||
; // empty
|
||||
}
|
||||
|
||||
void CHyprCustomLayout::fullscreenRequestForWindow(CWindow* pWindow, eFullscreenMode mode, bool on)
|
||||
{
|
||||
; // empty
|
||||
}
|
||||
|
||||
std::any CHyprCustomLayout::layoutMessage(SLayoutMessageHeader header, std::string content)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
SWindowRenderLayoutHints CHyprCustomLayout::requestRenderHints(CWindow* pWindow)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void CHyprCustomLayout::switchWindows(CWindow* pWindowA, CWindow* pWindowB)
|
||||
{
|
||||
; // empty
|
||||
}
|
||||
|
||||
void CHyprCustomLayout::alterSplitRatio(CWindow* pWindow, float delta, bool exact)
|
||||
{
|
||||
; // empty
|
||||
}
|
||||
|
||||
std::string CHyprCustomLayout::getLayoutName()
|
||||
{
|
||||
return "custom";
|
||||
}
|
||||
|
||||
void CHyprCustomLayout::replaceWindowDataWith(CWindow* from, CWindow* to)
|
||||
{
|
||||
; // empty
|
||||
}
|
||||
|
||||
void CHyprCustomLayout::onEnable()
|
||||
{
|
||||
for (auto& w : g_pCompositor->m_vWindows) {
|
||||
if (w->isHidden() || !w->m_bIsMapped || w->m_bFadingOut || w->m_bIsFloating)
|
||||
continue;
|
||||
|
||||
onWindowCreatedTiling(w.get());
|
||||
}
|
||||
}
|
||||
|
||||
void CHyprCustomLayout::onDisable()
|
||||
{
|
||||
m_vWindowData.clear();
|
||||
}
|
112
src/main.cpp
Normal file
112
src/main.cpp
Normal file
|
@ -0,0 +1,112 @@
|
|||
#define WLR_USE_UNSTABLE
|
||||
|
||||
#include "customDecoration.hpp"
|
||||
#include "customLayout.hpp"
|
||||
#include "globals.hpp"
|
||||
|
||||
#include <src/Compositor.hpp>
|
||||
#include <src/Window.hpp>
|
||||
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
|
||||
// Methods
|
||||
inline std::unique_ptr<CHyprCustomLayout> g_pCustomLayout;
|
||||
inline CFunctionHook* g_pFocusHook = nullptr;
|
||||
inline CFunctionHook* g_pMotionHook = nullptr;
|
||||
inline CFunctionHook* g_pMouseDownHook = nullptr;
|
||||
typedef void (*origFocusWindow)(void*, CWindow*, wlr_surface*);
|
||||
typedef void (*origMotion)(wlr_seat*, uint32_t, double, double);
|
||||
typedef void (*origMouseDownNormal)(void*, wlr_pointer_button_event*);
|
||||
|
||||
// Do NOT change this function.
|
||||
APICALL EXPORT std::string PLUGIN_API_VERSION()
|
||||
{
|
||||
return HYPRLAND_API_VERSION;
|
||||
}
|
||||
|
||||
static void onActiveWindowChange(void* self, std::any data)
|
||||
{
|
||||
try {
|
||||
auto* const PWINDOW = std::any_cast<CWindow*>(data);
|
||||
|
||||
HyprlandAPI::addNotification(PHANDLE, "[ExamplePlugin] Active window: " + (PWINDOW ? PWINDOW->m_szTitle : "None"), CColor{0.f, 0.5f, 1.f, 1.f}, 5000);
|
||||
}
|
||||
catch (std::bad_any_cast& e) {
|
||||
HyprlandAPI::addNotification(PHANDLE, "[ExamplePlugin] Active window: None", CColor{0.f, 0.5f, 1.f, 1.f}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
static void onNewWindow(void* self, std::any data)
|
||||
{
|
||||
auto* const PWINDOW = std::any_cast<CWindow*>(data);
|
||||
|
||||
HyprlandAPI::addWindowDecoration(PHANDLE, PWINDOW, new CCustomDecoration(PWINDOW));
|
||||
}
|
||||
|
||||
void hkFocusWindow(void* thisptr, CWindow* pWindow, wlr_surface* pSurface)
|
||||
{
|
||||
// HyprlandAPI::addNotification(PHANDLE, getFormat("FocusWindow with %lx %lx",
|
||||
// pWindow, pSurface), CColor{0.f, 1.f, 1.f, 1.f}, 5000);
|
||||
(*(origFocusWindow)g_pFocusHook->m_pOriginal)(thisptr, pWindow, pSurface);
|
||||
}
|
||||
|
||||
void hkNotifyMotion(wlr_seat* wlr_seat, uint32_t time_msec, double sx, double sy)
|
||||
{
|
||||
// HyprlandAPI::addNotification(PHANDLE, getFormat("NotifyMotion with %lf
|
||||
// %lf", sx, sy), CColor{0.f, 1.f, 1.f, 1.f}, 5000);
|
||||
(*(origMotion)g_pMotionHook->m_pOriginal)(wlr_seat, time_msec, sx, sy);
|
||||
}
|
||||
|
||||
void hkProcessMouseDownNormal(void* thisptr, wlr_pointer_button_event* e)
|
||||
{
|
||||
// HyprlandAPI::addNotification(PHANDLE, "Mouse down normal!", CColor{0.8f,
|
||||
// 0.2f, 0.5f, 1.0f}, 5000);
|
||||
(*(origMouseDownNormal)g_pMouseDownHook->m_pOriginal)(thisptr, e);
|
||||
}
|
||||
|
||||
APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle)
|
||||
{
|
||||
PHANDLE = handle;
|
||||
|
||||
HyprlandAPI::addNotification(PHANDLE, "Hello World from an example plugin!", CColor{0.f, 1.f, 1.f, 1.f}, 5000);
|
||||
|
||||
HyprlandAPI::registerCallbackDynamic(PHANDLE, "activeWindow", [&](void* self, std::any data) { onActiveWindowChange(self, data); });
|
||||
HyprlandAPI::registerCallbackDynamic(PHANDLE, "openWindow", [&](void* self, std::any data) { onNewWindow(self, data); });
|
||||
|
||||
g_pCustomLayout = std::make_unique<CHyprCustomLayout>();
|
||||
|
||||
HyprlandAPI::addLayout(PHANDLE, "custom", g_pCustomLayout.get());
|
||||
|
||||
HyprlandAPI::addConfigValue(PHANDLE, "plugin:example:border_color", SConfigValue{.intValue = configStringToInt("rgb(44ee44)")});
|
||||
|
||||
HyprlandAPI::addDispatcher(PHANDLE, "example", [](std::string arg) { HyprlandAPI::addNotification(PHANDLE, "Arg passed: " + arg, CColor{0.5f, 0.5f, 0.7f, 1.0f}, 5000); });
|
||||
|
||||
// Hook a public member
|
||||
g_pFocusHook = HyprlandAPI::createFunctionHook(PHANDLE, (void*)&CCompositor::focusWindow, (void*)&hkFocusWindow);
|
||||
// Hook a public non-member
|
||||
g_pMotionHook = HyprlandAPI::createFunctionHook(PHANDLE, (void*)&wlr_seat_pointer_notify_motion, (void*)&hkNotifyMotion);
|
||||
// Hook a private member (!WARNING: the signature may differ in clang. This
|
||||
// one is for gcc ONLY.)
|
||||
g_pMouseDownHook = HyprlandAPI::createFunctionHook(PHANDLE,
|
||||
HyprlandAPI::getFunctionAddressFromSignature(PHANDLE, "_ZN13CInputManager22processMouseDownNormalEP24wlr_pointer_"
|
||||
"button_event"),
|
||||
(void*)&hkProcessMouseDownNormal);
|
||||
|
||||
// fancy notifications
|
||||
HyprlandAPI::addNotificationV2(PHANDLE, {{"text", "Example hint"}, {"time", (uint64_t)10000}, {"color", CColor(0.2, 0.2, 0.9, 1.0)}, {"icon", ICON_HINT}});
|
||||
|
||||
// Enable our hooks
|
||||
g_pFocusHook->hook();
|
||||
g_pMotionHook->hook();
|
||||
g_pMouseDownHook->hook();
|
||||
|
||||
HyprlandAPI::reloadConfig();
|
||||
|
||||
return {"ExamplePlugin", "An example plugin", "Vaxry", "1.0"};
|
||||
}
|
||||
|
||||
APICALL EXPORT void PLUGIN_EXIT()
|
||||
{
|
||||
HyprlandAPI::invokeHyprctlCommand("seterror", "disable");
|
||||
}
|
Loading…
Reference in a new issue