mirror of
https://github.com/polybar/polybar.git
synced 2024-11-25 13:55:47 -05:00
feat(xbacklight): Change value on scroll
- Add action handlers for scroll up/down (conf: enable-sroll = true) - Use xcb_timestamps instead of the throttle util
This commit is contained in:
parent
2740e69a38
commit
f2bbd301f2
8 changed files with 184 additions and 70 deletions
|
@ -68,6 +68,7 @@ using std::to_string;
|
||||||
using std::strerror;
|
using std::strerror;
|
||||||
using std::getenv;
|
using std::getenv;
|
||||||
using std::thread;
|
using std::thread;
|
||||||
|
using std::exception;
|
||||||
|
|
||||||
using boost::optional;
|
using boost::optional;
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
#include "drawtypes/progressbar.hpp"
|
#include "drawtypes/progressbar.hpp"
|
||||||
#include "drawtypes/ramp.hpp"
|
#include "drawtypes/ramp.hpp"
|
||||||
#include "modules/meta.hpp"
|
#include "modules/meta.hpp"
|
||||||
#include "utils/throttle.hpp"
|
|
||||||
|
|
||||||
LEMONBUDDY_NS
|
LEMONBUDDY_NS
|
||||||
|
|
||||||
|
@ -35,21 +34,31 @@ namespace modules {
|
||||||
void setup();
|
void setup();
|
||||||
void handle(const evt::randr_notify& evt);
|
void handle(const evt::randr_notify& evt);
|
||||||
void update();
|
void update();
|
||||||
|
string get_output();
|
||||||
bool build(builder* builder, string tag) const;
|
bool build(builder* builder, string tag) const;
|
||||||
|
bool handle_event(string cmd);
|
||||||
|
bool receive_events() const {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr auto TAG_LABEL = "<label>";
|
static constexpr auto TAG_LABEL = "<label>";
|
||||||
static constexpr auto TAG_BAR = "<bar>";
|
static constexpr auto TAG_BAR = "<bar>";
|
||||||
static constexpr auto TAG_RAMP = "<ramp>";
|
static constexpr auto TAG_RAMP = "<ramp>";
|
||||||
|
|
||||||
throttle_util::throttle_t m_throttler;
|
static constexpr auto EVENT_SCROLLUP = "xbacklight+";
|
||||||
|
static constexpr auto EVENT_SCROLLDOWN = "xbacklight-";
|
||||||
|
|
||||||
connection& m_connection{configure_connection().create<connection&>()};
|
connection& m_connection{configure_connection().create<connection&>()};
|
||||||
monitor_t m_output;
|
monitor_t m_output;
|
||||||
|
xcb_window_t m_proxy;
|
||||||
|
xcb_timestamp_t m_timestamp;
|
||||||
|
|
||||||
ramp_t m_ramp;
|
ramp_t m_ramp;
|
||||||
label_t m_label;
|
label_t m_label;
|
||||||
progressbar_t m_progressbar;
|
progressbar_t m_progressbar;
|
||||||
|
|
||||||
|
bool m_scroll = true;
|
||||||
int m_percentage = 0;
|
int m_percentage = 0;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,12 @@ namespace graphics_util {
|
||||||
xcb_pixmap_t pixmap{0};
|
xcb_pixmap_t pixmap{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
void get_root_pixmap(connection& conn, root_pixmap* rpix);
|
bool create_window(connection& conn, xcb_window_t* win, int16_t x = 0, int16_t y = 0, uint16_t w = 1, uint16_t h = 1);
|
||||||
|
bool create_pixmap(connection& conn, xcb_drawable_t dst, uint16_t w, uint16_t h, xcb_pixmap_t* pixmap);
|
||||||
|
bool create_pixmap(connection& conn, xcb_drawable_t dst, uint16_t w, uint16_t h, uint8_t d, xcb_pixmap_t* pixmap);
|
||||||
|
bool create_gc(connection& conn, xcb_drawable_t drawable, xcb_gcontext_t* gc);
|
||||||
|
|
||||||
void simple_gc(connection& conn, xcb_drawable_t drawable, xcb_gcontext_t* gc);
|
bool get_root_pixmap(connection& conn, root_pixmap* rpix);
|
||||||
void simple_pixmap(connection& conn, xcb_window_t dst, int w, int h, xcb_pixmap_t* pixmap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LEMONBUDDY_NS_END
|
LEMONBUDDY_NS_END
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
LEMONBUDDY_NS
|
LEMONBUDDY_NS
|
||||||
|
|
||||||
struct backlight_values {
|
struct backlight_values {
|
||||||
|
uint32_t atom = 0;
|
||||||
uint32_t min = 0;
|
uint32_t min = 0;
|
||||||
uint32_t max = 0;
|
uint32_t max = 0;
|
||||||
uint32_t val = 0;
|
uint32_t val = 0;
|
||||||
|
|
|
@ -61,7 +61,7 @@ void eventloop::run(chrono::duration<double, std::milli> timeframe, int limit) {
|
||||||
evt = next;
|
evt = next;
|
||||||
break;
|
break;
|
||||||
} else if (compare_events(evt, next)) {
|
} else if (compare_events(evt, next)) {
|
||||||
m_log.trace("eventloop: Swallowing event within timeframe");
|
m_log.trace_x("eventloop: Swallowing event within timeframe");
|
||||||
evt = next;
|
evt = next;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#include "modules/xbacklight.hpp"
|
#include "modules/xbacklight.hpp"
|
||||||
#include "utils/math.hpp"
|
#include "utils/math.hpp"
|
||||||
|
#include "x11/graphics.hpp"
|
||||||
|
|
||||||
LEMONBUDDY_NS
|
LEMONBUDDY_NS
|
||||||
|
|
||||||
|
@ -23,26 +24,27 @@ namespace modules {
|
||||||
throw module_error("No matching output found for \"" + output + "\", stopping module...");
|
throw module_error("No matching output found for \"" + output + "\", stopping module...");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get flag to check if we should add scroll handlers for changing value
|
||||||
|
GET_CONFIG_VALUE(name(), m_scroll, "enable-scroll");
|
||||||
|
|
||||||
// Query randr for the backlight max and min value
|
// Query randr for the backlight max and min value
|
||||||
try {
|
try {
|
||||||
auto& backlight = m_output->backlight;
|
auto& backlight = m_output->backlight;
|
||||||
randr_util::get_backlight_range(m_connection, m_output, backlight);
|
randr_util::get_backlight_range(m_connection, m_output, backlight);
|
||||||
randr_util::get_backlight_value(m_connection, m_output, backlight);
|
randr_util::get_backlight_value(m_connection, m_output, backlight);
|
||||||
} catch (const std::exception& err) {
|
} catch (const exception& err) {
|
||||||
throw module_error("No backlight data found for \"" + output + "\", stopping module...");
|
throw module_error("No backlight data found for \"" + output + "\", stopping module...");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect with the event registry and tell randr that we
|
// Create window that will proxy all RandR notify events
|
||||||
// want to get notified when an output property gets modified
|
if (!graphics_util::create_window(m_connection, &m_proxy, -1, -1, 1, 1)) {
|
||||||
m_connection.attach_sink(this, 1);
|
throw module_error("Failed to create event proxy");
|
||||||
m_connection.select_input_checked(
|
}
|
||||||
m_connection.screen()->root, XCB_RANDR_NOTIFY_MASK_OUTPUT_PROPERTY);
|
|
||||||
|
|
||||||
// Create a throttle so that we limit the amount of events
|
// Connect with the event registry and make sure we get
|
||||||
// to handle since randr can burst out quite a few
|
// notified when a RandR output property gets modified
|
||||||
// We will allow 1 event per 60 ms. The updates still look smooth
|
m_connection.attach_sink(this, 1);
|
||||||
// using this setting which is important.
|
m_connection.select_input_checked(m_proxy, XCB_RANDR_NOTIFY_MASK_OUTPUT_PROPERTY);
|
||||||
m_throttler = throttle_util::make_throttler(1, 60ms);
|
|
||||||
|
|
||||||
// Add formats and elements
|
// Add formats and elements
|
||||||
m_formatter->add(DEFAULT_FORMAT, TAG_LABEL, {TAG_LABEL, TAG_BAR, TAG_RAMP});
|
m_formatter->add(DEFAULT_FORMAT, TAG_LABEL, {TAG_LABEL, TAG_BAR, TAG_RAMP});
|
||||||
|
@ -64,22 +66,28 @@ namespace modules {
|
||||||
void xbacklight_module::handle(const evt::randr_notify& evt) {
|
void xbacklight_module::handle(const evt::randr_notify& evt) {
|
||||||
if (evt->subCode != XCB_RANDR_NOTIFY_OUTPUT_PROPERTY)
|
if (evt->subCode != XCB_RANDR_NOTIFY_OUTPUT_PROPERTY)
|
||||||
return;
|
return;
|
||||||
|
else if (evt->u.op.status != XCB_PROPERTY_NEW_VALUE)
|
||||||
|
return;
|
||||||
|
else if (evt->u.op.window != m_proxy)
|
||||||
|
return;
|
||||||
else if (evt->u.op.output != m_output->randr_output)
|
else if (evt->u.op.output != m_output->randr_output)
|
||||||
return;
|
return;
|
||||||
else if (evt->u.op.atom == Backlight)
|
else if (evt->u.op.atom != m_output->backlight.atom)
|
||||||
update();
|
return;
|
||||||
else if (evt->u.op.atom == BACKLIGHT)
|
else if (evt->u.op.timestamp <= m_timestamp)
|
||||||
update();
|
return;
|
||||||
|
|
||||||
|
// Store the timestamp with a throttle offset (ms)
|
||||||
|
m_timestamp = evt->u.op.timestamp + 50;
|
||||||
|
|
||||||
|
// Fetch the new values
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query the RandR extension for the new values
|
* Query the RandR extension for the new values
|
||||||
*/
|
*/
|
||||||
void xbacklight_module::update() {
|
void xbacklight_module::update() {
|
||||||
// Test if we are allowed to handle the event
|
|
||||||
if (!m_throttler->passthrough(throttle_util::strategy::try_once_or_leave_yolo{}))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Query for the new backlight value
|
// Query for the new backlight value
|
||||||
auto& bl = m_output->backlight;
|
auto& bl = m_output->backlight;
|
||||||
randr_util::get_backlight_value(m_connection, m_output, bl);
|
randr_util::get_backlight_value(m_connection, m_output, bl);
|
||||||
|
@ -96,6 +104,29 @@ namespace modules {
|
||||||
broadcast();
|
broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the module output
|
||||||
|
*/
|
||||||
|
string xbacklight_module::get_output() {
|
||||||
|
if (m_scroll) {
|
||||||
|
if (m_percentage < 100)
|
||||||
|
m_builder->cmd(mousebtn::SCROLL_UP, EVENT_SCROLLUP);
|
||||||
|
if (m_percentage > 0)
|
||||||
|
m_builder->cmd(mousebtn::SCROLL_DOWN, EVENT_SCROLLDOWN);
|
||||||
|
|
||||||
|
m_builder->node(static_module::get_output());
|
||||||
|
|
||||||
|
if (m_percentage < 100)
|
||||||
|
m_builder->cmd_close(true);
|
||||||
|
if (m_percentage > 0)
|
||||||
|
m_builder->cmd_close(true);
|
||||||
|
} else {
|
||||||
|
m_builder->node(static_module::get_output());
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_builder->flush();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Output content as defined in the config
|
* Output content as defined in the config
|
||||||
*/
|
*/
|
||||||
|
@ -110,6 +141,36 @@ namespace modules {
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process scroll events by changing backlight value
|
||||||
|
*/
|
||||||
|
bool xbacklight_module::handle_event(string cmd) {
|
||||||
|
int value_mod = 0;
|
||||||
|
|
||||||
|
if (cmd == EVENT_SCROLLUP) {
|
||||||
|
value_mod = 10;
|
||||||
|
m_log.info("%s: Increasing value by %i%", name(), value_mod);
|
||||||
|
} else if (cmd == EVENT_SCROLLDOWN) {
|
||||||
|
value_mod = -10;
|
||||||
|
m_log.info("%s: Decreasing value by %i%", name(), -value_mod);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const int new_perc = math_util::cap(m_percentage + value_mod, 0, 100);
|
||||||
|
const int new_value = math_util::percentage_to_value<int>(new_perc, m_output->backlight.max);
|
||||||
|
const int values[1]{new_value};
|
||||||
|
|
||||||
|
m_connection.change_output_property_checked(
|
||||||
|
m_output->randr_output, m_output->backlight.atom, XCB_ATOM_INTEGER, 32, XCB_PROP_MODE_REPLACE, 1, values);
|
||||||
|
} catch (const exception& err) {
|
||||||
|
m_log.err("%s: %s", name(), err.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LEMONBUDDY_NS_END
|
LEMONBUDDY_NS_END
|
||||||
|
|
|
@ -9,10 +9,73 @@
|
||||||
LEMONBUDDY_NS
|
LEMONBUDDY_NS
|
||||||
|
|
||||||
namespace graphics_util {
|
namespace graphics_util {
|
||||||
|
/**
|
||||||
|
* Create a basic window
|
||||||
|
*/
|
||||||
|
bool create_window(connection& conn, xcb_window_t* win, int16_t x, int16_t y, uint16_t w, uint16_t h) {
|
||||||
|
try {
|
||||||
|
auto root = conn.screen()->root;
|
||||||
|
auto copy = XCB_COPY_FROM_PARENT;
|
||||||
|
*win = conn.generate_id();
|
||||||
|
conn.create_window_checked(copy, *win, root, x, y, w, h, 0, copy, copy, 0, nullptr);
|
||||||
|
return true;
|
||||||
|
} catch (const exception& err) {
|
||||||
|
*win = XCB_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a basic pixmap with the same depth as the
|
||||||
|
* root depth of the default screen
|
||||||
|
*/
|
||||||
|
bool create_pixmap(connection& conn, xcb_drawable_t dst, uint16_t w, uint16_t h, xcb_pixmap_t* pixmap) {
|
||||||
|
return graphics_util::create_pixmap(conn, dst, w, h, conn.screen()->root_depth, pixmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a basic pixmap with specific depth
|
||||||
|
*/
|
||||||
|
bool create_pixmap(connection& conn, xcb_drawable_t dst, uint16_t w, uint16_t h, uint8_t d, xcb_pixmap_t* pixmap) {
|
||||||
|
try {
|
||||||
|
*pixmap = conn.generate_id();
|
||||||
|
conn.create_pixmap_checked(d, *pixmap, dst, w, h);
|
||||||
|
return true;
|
||||||
|
} catch (const exception& err) {
|
||||||
|
*pixmap = XCB_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a basic gc
|
||||||
|
*/
|
||||||
|
bool create_gc(connection& conn, xcb_drawable_t drawable, xcb_gcontext_t* gc) {
|
||||||
|
try {
|
||||||
|
xcb_params_gc_t params;
|
||||||
|
|
||||||
|
uint32_t mask = 0;
|
||||||
|
uint32_t values[32];
|
||||||
|
|
||||||
|
XCB_AUX_ADD_PARAM(&mask, ¶ms, graphics_exposures, false);
|
||||||
|
xutils::pack_values(mask, ¶ms, values);
|
||||||
|
|
||||||
|
*gc = conn.generate_id();
|
||||||
|
conn.create_gc_checked(*gc, drawable, mask, values);
|
||||||
|
return true;
|
||||||
|
} catch (const exception& err) {
|
||||||
|
*gc = XCB_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query for the root window pixmap
|
* Query for the root window pixmap
|
||||||
*/
|
*/
|
||||||
void get_root_pixmap(connection& conn, root_pixmap* rpix) {
|
bool get_root_pixmap(connection& conn, root_pixmap* rpix) {
|
||||||
auto screen = conn.screen();
|
auto screen = conn.screen();
|
||||||
const xcb_atom_t pixmap_properties[3]{ESETROOT_PMAP_ID, _XROOTMAP_ID, _XSETROOT_ID};
|
const xcb_atom_t pixmap_properties[3]{ESETROOT_PMAP_ID, _XROOTMAP_ID, _XSETROOT_ID};
|
||||||
for (auto&& property : pixmap_properties) {
|
for (auto&& property : pixmap_properties) {
|
||||||
|
@ -25,15 +88,15 @@ namespace graphics_util {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rpix->pixmap) {
|
if (!rpix->pixmap) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto cookie = xcb_get_geometry(conn, rpix->pixmap);
|
auto cookie = xcb_get_geometry(conn, rpix->pixmap);
|
||||||
auto reply = xcb_get_geometry_reply(conn, cookie, nullptr);
|
auto reply = xcb_get_geometry_reply(conn, cookie, nullptr);
|
||||||
|
|
||||||
if (!reply) {
|
if (!reply) {
|
||||||
rpix->pixmap = 0;
|
rpix->pixmap = XCB_NONE;
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
rpix->depth = reply->depth;
|
rpix->depth = reply->depth;
|
||||||
|
@ -41,38 +104,8 @@ namespace graphics_util {
|
||||||
rpix->height = reply->height;
|
rpix->height = reply->height;
|
||||||
rpix->x = reply->x;
|
rpix->x = reply->x;
|
||||||
rpix->y = reply->y;
|
rpix->y = reply->y;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return true;
|
||||||
* Create a basic gc
|
|
||||||
*/
|
|
||||||
void simple_gc(connection& conn, xcb_drawable_t drawable, xcb_gcontext_t* gc) {
|
|
||||||
xcb_params_gc_t params;
|
|
||||||
|
|
||||||
uint32_t mask = 0;
|
|
||||||
uint32_t values[32];
|
|
||||||
|
|
||||||
XCB_AUX_ADD_PARAM(&mask, ¶ms, graphics_exposures, false);
|
|
||||||
xutils::pack_values(mask, ¶ms, values);
|
|
||||||
|
|
||||||
try {
|
|
||||||
*gc = conn.generate_id();
|
|
||||||
conn.create_gc_checked(*gc, drawable, mask, values);
|
|
||||||
} catch (const std::exception& err) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a basic pixmap
|
|
||||||
*/
|
|
||||||
void simple_pixmap(connection& conn, xcb_window_t dst, int w, int h, xcb_pixmap_t* pixmap) {
|
|
||||||
try {
|
|
||||||
*pixmap = conn.generate_id();
|
|
||||||
conn.create_pixmap_checked(conn.screen()->root_depth, *pixmap, dst, w, h);
|
|
||||||
} catch (const std::exception& err) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,34 +53,41 @@ namespace randr_util {
|
||||||
* Get backlight value range for given output
|
* Get backlight value range for given output
|
||||||
*/
|
*/
|
||||||
void get_backlight_range(connection& conn, const monitor_t& mon, backlight_values& dst) {
|
void get_backlight_range(connection& conn, const monitor_t& mon, backlight_values& dst) {
|
||||||
auto reply = conn.query_output_property(mon->randr_output, Backlight);
|
auto atom = Backlight;
|
||||||
|
auto reply = conn.query_output_property(mon->randr_output, atom);
|
||||||
|
|
||||||
dst.min = 0;
|
dst.min = 0;
|
||||||
dst.max = 0;
|
dst.max = 0;
|
||||||
|
|
||||||
if (!reply->range || reply->length != 2)
|
if (!reply->range || reply->length != 2) {
|
||||||
reply = conn.query_output_property(mon->randr_output, BACKLIGHT);
|
atom = BACKLIGHT;
|
||||||
if (!reply->range || reply->length != 2)
|
reply = conn.query_output_property(mon->randr_output, atom);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reply->range || reply->length != 2) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto range = reply.valid_values().begin();
|
auto range = reply.valid_values().begin();
|
||||||
|
|
||||||
dst.min = static_cast<uint32_t>(*range++);
|
dst.min = static_cast<uint32_t>(*range++);
|
||||||
dst.max = static_cast<uint32_t>(*range);
|
dst.max = static_cast<uint32_t>(*range);
|
||||||
|
dst.atom = atom;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get backlight value for given output
|
* Get backlight value for given output
|
||||||
*/
|
*/
|
||||||
void get_backlight_value(connection& conn, const monitor_t& mon, backlight_values& dst) {
|
void get_backlight_value(connection& conn, const monitor_t& mon, backlight_values& dst) {
|
||||||
auto reply = conn.get_output_property(mon->randr_output, Backlight, XCB_ATOM_NONE, 0, 4, 0, 0);
|
dst.val = 0;
|
||||||
|
|
||||||
|
if (!dst.atom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto reply = conn.get_output_property(mon->randr_output, dst.atom, XCB_ATOM_NONE, 0, 4, 0, 0);
|
||||||
|
|
||||||
if (reply->num_items != 1 || reply->format != 32 || reply->type != XCB_ATOM_INTEGER)
|
|
||||||
reply = conn.get_output_property(mon->randr_output, BACKLIGHT, XCB_ATOM_NONE, 0, 4, 0, 0);
|
|
||||||
if (reply->num_items == 1 && reply->format == 32 && reply->type == XCB_ATOM_INTEGER)
|
if (reply->num_items == 1 && reply->format == 32 && reply->type == XCB_ATOM_INTEGER)
|
||||||
dst.val = *reinterpret_cast<uint32_t*>(xcb_randr_get_output_property_data(reply.get().get()));
|
dst.val = *reinterpret_cast<uint32_t*>(xcb_randr_get_output_property_data(reply.get().get()));
|
||||||
else
|
|
||||||
dst.val = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue