Fix formatting
This commit is contained in:
parent
8e5574ab49
commit
2dfec75038
5 changed files with 135 additions and 182 deletions
|
@ -37,7 +37,8 @@ typedef void (*animation_api_prep_next_frame)(const struct device *dev);
|
|||
*
|
||||
* @see animation_prep_next_frame() for argument descriptions.
|
||||
*/
|
||||
typedef void (*animation_api_get_pixel)(const struct device *dev, const struct animation_pixel_position *pixel_position,
|
||||
typedef void (*animation_api_get_pixel)(const struct device *dev,
|
||||
const struct animation_pixel_position *pixel_position,
|
||||
struct zmk_color_rgb *value);
|
||||
|
||||
struct animation_api {
|
||||
|
@ -50,7 +51,7 @@ struct animation_api {
|
|||
* @param dev [description]
|
||||
*/
|
||||
static inline void animation_prep_next_frame(const struct device *dev) {
|
||||
const struct animation_api *api = (const struct animation_api *) dev->api;
|
||||
const struct animation_api *api = (const struct animation_api *)dev->api;
|
||||
|
||||
return api->prep_next_frame(dev);
|
||||
}
|
||||
|
@ -60,9 +61,10 @@ static inline void animation_prep_next_frame(const struct device *dev) {
|
|||
* @param dev [description]
|
||||
* @param pixel [description]
|
||||
*/
|
||||
static inline void animation_get_pixel(const struct device *dev, const struct animation_pixel_position *pixel_position,
|
||||
struct zmk_color_rgb *value) {
|
||||
const struct animation_api *api = (const struct animation_api *) dev->api;
|
||||
static inline void animation_get_pixel(const struct device *dev,
|
||||
const struct animation_pixel_position *pixel_position,
|
||||
struct zmk_color_rgb *value) {
|
||||
const struct animation_api *api = (const struct animation_api *)dev->api;
|
||||
|
||||
return api->get_pixel(dev, pixel_position, value);
|
||||
}
|
||||
|
|
|
@ -34,8 +34,8 @@ struct zmk_color_hsl {
|
|||
/**
|
||||
* Converts color from HSL to RGB.
|
||||
*
|
||||
* @param hsl [description]
|
||||
* @param rgb [description]
|
||||
* @param hsl Color to convert
|
||||
* @param rgb Converted color
|
||||
*/
|
||||
void zmk_hsl_to_rgb(const struct zmk_color_hsl *hsl, struct zmk_color_rgb *rgb);
|
||||
|
||||
|
@ -43,17 +43,17 @@ void zmk_hsl_to_rgb(const struct zmk_color_hsl *hsl, struct zmk_color_rgb *rgb);
|
|||
* Converts the internal RGB representation into a led_rgb struct
|
||||
* for use with led_strip drivers.
|
||||
*
|
||||
* @param rgb [description]
|
||||
* @param led [description]
|
||||
* @param rgb Color to convert
|
||||
* @param led Converted color
|
||||
*/
|
||||
void zmk_rgb_to_led_rgb(const struct zmk_color_rgb *rgb, struct led_rgb *led);
|
||||
|
||||
/**
|
||||
* Returns true if two HSL colors are the same.
|
||||
*
|
||||
* @param a [description]
|
||||
* @param b [description]
|
||||
* @return [description]
|
||||
* @param a HSL color to compare
|
||||
* @param b HSL color to compare
|
||||
* @return True when colors share the same values
|
||||
*/
|
||||
bool zmk_cmp_hsl(const struct zmk_color_hsl *a, const struct zmk_color_hsl *b);
|
||||
|
||||
|
@ -61,10 +61,10 @@ bool zmk_cmp_hsl(const struct zmk_color_hsl *a, const struct zmk_color_hsl *b);
|
|||
* Perform linear interpolation between HSL values of two colors
|
||||
* at a given distance (step) and store the resulting value in the given pointer.
|
||||
*
|
||||
* @param from [description]
|
||||
* @param to [description]
|
||||
* @param result [description]
|
||||
* @param step [description]
|
||||
* @param from HSL color to interpolate
|
||||
* @param to HSL color to interpolate
|
||||
* @param result Resulting HSL color
|
||||
* @param step Interpolation step
|
||||
*/
|
||||
void zmk_interpolate_hsl(const struct zmk_color_hsl *from, const struct zmk_color_hsl *to,
|
||||
struct zmk_color_hsl *result, float step);
|
||||
struct zmk_color_hsl *result, float step);
|
||||
|
|
|
@ -22,30 +22,29 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
|||
|
||||
// Zephyr 2.7.0 comes with DT_INST_FOREACH_PROP_ELEM
|
||||
// that we can't use quite yet as we're still on 2.5.*
|
||||
#define ZMK_DT_INST_FOREACH_PROP_ELEM(inst, prop, fn) \
|
||||
UTIL_LISTIFY(DT_INST_PROP_LEN(inst, prop), fn, DT_DRV_INST(inst), prop)
|
||||
#define ZMK_DT_INST_FOREACH_PROP_ELEM(inst, prop, fn) \
|
||||
UTIL_LISTIFY(DT_INST_PROP_LEN(inst, prop), fn, DT_DRV_INST(inst), prop)
|
||||
|
||||
#define PHANDLE_TO_DEVICE(idx, node_id, prop) \
|
||||
DEVICE_DT_GET(DT_PHANDLE_BY_IDX(node_id, prop, idx)),
|
||||
#define PHANDLE_TO_DEVICE(idx, node_id, prop) DEVICE_DT_GET(DT_PHANDLE_BY_IDX(node_id, prop, idx)),
|
||||
|
||||
#define PHANDLE_TO_CHAIN_LENGTH(idx, node_id, prop) \
|
||||
DT_PROP_BY_PHANDLE_IDX(node_id, prop, idx, chain_length),
|
||||
#define PHANDLE_TO_CHAIN_LENGTH(idx, node_id, prop) \
|
||||
DT_PROP_BY_PHANDLE_IDX(node_id, prop, idx, chain_length),
|
||||
|
||||
#define PHANDLE_TO_PIXEL(idx, node_id, prop) \
|
||||
{ \
|
||||
.animation = PHANDLE_TO_DEVICE(idx, node_id, prop) \
|
||||
.position = { \
|
||||
.x = DT_PHA_BY_IDX(node_id, prop, idx, position_x),\
|
||||
.y = DT_PHA_BY_IDX(node_id, prop, idx, position_y),\
|
||||
}, \
|
||||
},
|
||||
#define PHANDLE_TO_PIXEL(idx, node_id, prop) \
|
||||
{ \
|
||||
.animation = DEVICE_DT_GET(DT_PHANDLE_BY_IDX(node_id, prop, idx)), \
|
||||
.position = \
|
||||
{ \
|
||||
.x = DT_PHA_BY_IDX(node_id, prop, idx, position_x), \
|
||||
.y = DT_PHA_BY_IDX(node_id, prop, idx, position_y), \
|
||||
}, \
|
||||
},
|
||||
|
||||
/**
|
||||
* LED Driver device pointers.
|
||||
*/
|
||||
static const struct device *drivers[] = {
|
||||
ZMK_DT_INST_FOREACH_PROP_ELEM(0, drivers, PHANDLE_TO_DEVICE)
|
||||
};
|
||||
ZMK_DT_INST_FOREACH_PROP_ELEM(0, drivers, PHANDLE_TO_DEVICE)};
|
||||
|
||||
/**
|
||||
* Size of the LED driver device pointers array.
|
||||
|
@ -56,15 +55,13 @@ static const size_t drivers_size = DT_INST_PROP_LEN(0, drivers);
|
|||
* Array containing the number of LEDs handled by each device.
|
||||
*/
|
||||
static const uint8_t pixels_per_driver[] = {
|
||||
ZMK_DT_INST_FOREACH_PROP_ELEM(0, drivers, PHANDLE_TO_CHAIN_LENGTH)
|
||||
};
|
||||
ZMK_DT_INST_FOREACH_PROP_ELEM(0, drivers, PHANDLE_TO_CHAIN_LENGTH)};
|
||||
|
||||
/**
|
||||
* Pointers to all active animation devices.
|
||||
*/
|
||||
static const struct device *animations[] = {
|
||||
ZMK_DT_INST_FOREACH_PROP_ELEM(0, animations, PHANDLE_TO_DEVICE)
|
||||
};
|
||||
ZMK_DT_INST_FOREACH_PROP_ELEM(0, animations, PHANDLE_TO_DEVICE)};
|
||||
|
||||
/**
|
||||
* Size of the animation device pointers array.
|
||||
|
@ -75,8 +72,7 @@ static const size_t animations_size = DT_INST_PROP_LEN(0, animations);
|
|||
* Pixel configuration.
|
||||
*/
|
||||
static const struct animation_pixel pixels[] = {
|
||||
ZMK_DT_INST_FOREACH_PROP_ELEM(0, pixels, PHANDLE_TO_PIXEL)
|
||||
};
|
||||
ZMK_DT_INST_FOREACH_PROP_ELEM(0, pixels, PHANDLE_TO_PIXEL)};
|
||||
|
||||
/**
|
||||
* Size of the pixels array.
|
||||
|
@ -89,49 +85,43 @@ static const size_t pixels_size = DT_INST_PROP_LEN(0, pixels);
|
|||
static struct led_rgb px_buffer[DT_INST_PROP_LEN(0, pixels)];
|
||||
|
||||
static void zmk_animation_tick(struct k_work *work) {
|
||||
for (size_t i = 0; i < animations_size; ++i) {
|
||||
animation_prep_next_frame(animations[i]);
|
||||
}
|
||||
for (size_t i = 0; i < animations_size; ++i) {
|
||||
animation_prep_next_frame(animations[i]);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < pixels_size; ++i) {
|
||||
struct zmk_color_rgb rgb = {
|
||||
.r = 0,
|
||||
.g = 0,
|
||||
.b = 0,
|
||||
};
|
||||
for (size_t i = 0; i < pixels_size; ++i) {
|
||||
struct zmk_color_rgb rgb = {
|
||||
.r = 0,
|
||||
.g = 0,
|
||||
.b = 0,
|
||||
};
|
||||
|
||||
animation_get_pixel(pixels[i].animation, &pixels[i].position, &rgb);
|
||||
animation_get_pixel(pixels[i].animation, &pixels[i].position, &rgb);
|
||||
|
||||
zmk_rgb_to_led_rgb(&rgb, &px_buffer[i]);
|
||||
}
|
||||
zmk_rgb_to_led_rgb(&rgb, &px_buffer[i]);
|
||||
}
|
||||
|
||||
size_t pixels_updated = 0;
|
||||
size_t pixels_updated = 0;
|
||||
|
||||
for (size_t i = 0; i < drivers_size; ++i) {
|
||||
led_strip_update_rgb(
|
||||
drivers[i],
|
||||
&px_buffer[pixels_updated],
|
||||
pixels_per_driver[i]
|
||||
);
|
||||
for (size_t i = 0; i < drivers_size; ++i) {
|
||||
led_strip_update_rgb(drivers[i], &px_buffer[pixels_updated], pixels_per_driver[i]);
|
||||
|
||||
pixels_updated += (size_t) pixels_per_driver;
|
||||
}
|
||||
pixels_updated += (size_t)pixels_per_driver;
|
||||
}
|
||||
}
|
||||
|
||||
K_WORK_DEFINE(animation_work, zmk_animation_tick);
|
||||
|
||||
static void zmk_animation_tick_handler(struct k_timer *timer) {
|
||||
k_work_submit(&animation_work);
|
||||
}
|
||||
static void zmk_animation_tick_handler(struct k_timer *timer) { k_work_submit(&animation_work); }
|
||||
|
||||
K_TIMER_DEFINE(animation_tick, zmk_animation_tick_handler, NULL);
|
||||
|
||||
static int zmk_animation_init(const struct device *dev) {
|
||||
LOG_INF("ZMK Animation Ready");
|
||||
LOG_INF("ZMK Animation Ready");
|
||||
|
||||
k_timer_start(&animation_tick, K_NO_WAIT, K_MSEC(1000 / CONFIG_ZMK_ANIMATION_FPS));
|
||||
k_timer_start(&animation_tick, K_NO_WAIT, K_MSEC(1000 / CONFIG_ZMK_ANIMATION_FPS));
|
||||
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
SYS_INIT(zmk_animation_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
|
||||
|
|
|
@ -45,12 +45,9 @@ static void animation_solid_prep_next_frame(const struct device *dev) {
|
|||
|
||||
struct zmk_color_hsl next_hsl;
|
||||
|
||||
zmk_interpolate_hsl(
|
||||
&config->colors[from],
|
||||
&config->colors[to],
|
||||
&next_hsl,
|
||||
(data->counter % config->transition_duration) / (float) config->transition_duration
|
||||
);
|
||||
zmk_interpolate_hsl(&config->colors[from], &config->colors[to], &next_hsl,
|
||||
(data->counter % config->transition_duration) /
|
||||
(float)config->transition_duration);
|
||||
|
||||
data->has_changed = !zmk_cmp_hsl(&data->current_hsl, &next_hsl);
|
||||
|
||||
|
@ -60,8 +57,9 @@ static void animation_solid_prep_next_frame(const struct device *dev) {
|
|||
data->counter = (data->counter + 1) % config->duration;
|
||||
}
|
||||
|
||||
static void animation_solid_get_pixel(const struct device *dev, const struct animation_pixel_position *position,
|
||||
struct zmk_color_rgb *value) {
|
||||
static void animation_solid_get_pixel(const struct device *dev,
|
||||
const struct animation_pixel_position *position,
|
||||
struct zmk_color_rgb *value) {
|
||||
const struct animation_solid_data *data = dev->data;
|
||||
|
||||
value->r = data->current_rgb.r;
|
||||
|
@ -86,59 +84,23 @@ static const struct animation_api animation_solid_api = {
|
|||
.get_pixel = animation_solid_get_pixel,
|
||||
};
|
||||
|
||||
#define ANIMATION_SOLID_DEVICE(idx) \
|
||||
\
|
||||
static struct animation_solid_data animation_solid_##idx##_data; \
|
||||
\
|
||||
static uint32_t animation_solid_##idx##_colors[DT_INST_PROP_LEN(idx, colors)] = DT_INST_PROP(idx, colors); \
|
||||
\
|
||||
static struct animation_solid_config animation_solid_##idx##_config = { \
|
||||
.colors = (struct zmk_color_hsl *) animation_solid_##idx##_colors, \
|
||||
.num_colors = DT_INST_PROP_LEN(idx, colors), \
|
||||
.duration = DT_INST_PROP(idx, duration) * CONFIG_ZMK_ANIMATION_FPS, \
|
||||
.transition_duration = (DT_INST_PROP(idx, duration) * CONFIG_ZMK_ANIMATION_FPS) / DT_INST_PROP_LEN(idx, colors), \
|
||||
}; \
|
||||
\
|
||||
DEVICE_DT_INST_DEFINE(idx, &animation_solid_init, NULL, &animation_solid_##idx##_data, \
|
||||
&animation_solid_##idx##_config, POST_KERNEL, CONFIG_LED_STRIP_INIT_PRIORITY, \
|
||||
&animation_solid_api);
|
||||
#define ANIMATION_SOLID_DEVICE(idx) \
|
||||
\
|
||||
static struct animation_solid_data animation_solid_##idx##_data; \
|
||||
\
|
||||
static uint32_t animation_solid_##idx##_colors[DT_INST_PROP_LEN(idx, colors)] = \
|
||||
DT_INST_PROP(idx, colors); \
|
||||
\
|
||||
static struct animation_solid_config animation_solid_##idx##_config = { \
|
||||
.colors = (struct zmk_color_hsl *)animation_solid_##idx##_colors, \
|
||||
.num_colors = DT_INST_PROP_LEN(idx, colors), \
|
||||
.duration = DT_INST_PROP(idx, duration) * CONFIG_ZMK_ANIMATION_FPS, \
|
||||
.transition_duration = (DT_INST_PROP(idx, duration) * CONFIG_ZMK_ANIMATION_FPS) / \
|
||||
DT_INST_PROP_LEN(idx, colors), \
|
||||
}; \
|
||||
\
|
||||
DEVICE_DT_INST_DEFINE(idx, &animation_solid_init, NULL, &animation_solid_##idx##_data, \
|
||||
&animation_solid_##idx##_config, POST_KERNEL, \
|
||||
CONFIG_APPLICATION_INIT_PRIORITY, &animation_solid_api);
|
||||
|
||||
DT_INST_FOREACH_STATUS_OKAY(ANIMATION_SOLID_DEVICE);
|
||||
|
||||
|
||||
// To do:
|
||||
//
|
||||
// STEP 1: single animation
|
||||
// - Start with a single animation, just color
|
||||
// - Add layer for taking the output from here and putting it to the led strip
|
||||
// - Make it work
|
||||
//
|
||||
// STEP 2: areas, in fact, instead of defining them explicitly we can just use appropriate x,y coordinates and animation.
|
||||
// - Split keyboard in two independent areas
|
||||
// - Make it work
|
||||
//
|
||||
// STEP 3: add additional animation effects
|
||||
// - Basically, carry over rgb_underglow.
|
||||
// - Make it work
|
||||
//
|
||||
// STEP 4: add animation triggers
|
||||
// - Allow an animation to be triggered by behaviors or key-presses
|
||||
// - Make it work
|
||||
//
|
||||
// STEP 5: add animation layers and a MULTIPLY mode (again, opacity would be set on individual pixels so... that affects some optimizations I guess)
|
||||
// - Normal mode: overrides layers below
|
||||
// - Multiply mode: auguments whatever is below (opacity, whatever)
|
||||
//
|
||||
// Voila! Animation composition!
|
||||
//
|
||||
// STEP 6, BONUS!:
|
||||
// - Figure out a way to switch animations during runtime?
|
||||
//
|
||||
// Notes:
|
||||
// - Any animation settings go into 'driver' config & data, so they can be updated at runtime.
|
||||
// - Main limitation is space, so the amount of different animations one can have loaded
|
||||
//
|
||||
// More notes:
|
||||
// - Solid color would be one animation (just transitions between colors)
|
||||
// - Gradient (SPECTRUM) would be another, you choose how they're distributed accross the keys and if they move?
|
||||
// - Effects like 'breathe' can be implemented by specifying #000 as one of the colors or using a multiply layer?
|
||||
|
|
|
@ -9,98 +9,97 @@
|
|||
#include <zmk/animation.h>
|
||||
|
||||
static float fmod(float a, float b) {
|
||||
float mod = a < 0 ? -a : a;
|
||||
float x = b < 0 ? -b : b;
|
||||
float mod = a < 0 ? -a : a;
|
||||
float x = b < 0 ? -b : b;
|
||||
|
||||
while (mod >= x) {
|
||||
mod = mod - x;
|
||||
}
|
||||
while (mod >= x) {
|
||||
mod = mod - x;
|
||||
}
|
||||
|
||||
return a < 0 ? -mod : mod;
|
||||
return a < 0 ? -mod : mod;
|
||||
}
|
||||
|
||||
static float fabs(float a) {
|
||||
return a < 0 ? -a : a;
|
||||
}
|
||||
static float fabs(float a) { return a < 0 ? -a : a; }
|
||||
|
||||
/**
|
||||
* HSL chosen over HSV/HSB as it shares the same parameters with LCh or HSLuv.
|
||||
* The latter color spaces could be interesting to experiment with because of their
|
||||
* perceptual uniformity, but it would come at the cost of some performance.
|
||||
* Using the same parameters would make it easy to toggle any such behavior using a single config flag.
|
||||
* Using the same parameters would make it easy to toggle any such behavior
|
||||
* using a single config flag.
|
||||
*
|
||||
* Algorithm source: https://www.tlbx.app/color-converter
|
||||
*/
|
||||
void zmk_hsl_to_rgb(const struct zmk_color_hsl *hsl, struct zmk_color_rgb *rgb) {
|
||||
float s = (float) hsl->s / 100;
|
||||
float l = (float) hsl->l / 100;
|
||||
float s = (float)hsl->s / 100;
|
||||
float l = (float)hsl->l / 100;
|
||||
|
||||
float a = (float) hsl->h / 60;
|
||||
float chroma = s * (1 - fabs(2 * l - 1));
|
||||
float x = chroma * (1 - fabs(fmod(a, 2) - 1));
|
||||
float m = l - chroma / 2;
|
||||
float a = (float)hsl->h / 60;
|
||||
float chroma = s * (1 - fabs(2 * l - 1));
|
||||
float x = chroma * (1 - fabs(fmod(a, 2) - 1));
|
||||
float m = l - chroma / 2;
|
||||
|
||||
switch ((uint8_t) a % 6) {
|
||||
case 0:
|
||||
rgb->r = m + chroma;
|
||||
rgb->g = m + x;
|
||||
rgb->b = m;
|
||||
break;
|
||||
case 1:
|
||||
rgb->r = m + x;
|
||||
rgb->g = m + chroma;
|
||||
rgb->b = m;
|
||||
break;
|
||||
case 2:
|
||||
rgb->r = m;
|
||||
rgb->g = m + chroma;
|
||||
rgb->b = m + x;
|
||||
break;
|
||||
case 3:
|
||||
rgb->r = m;
|
||||
rgb->g = m + x;
|
||||
rgb->b = m + chroma;
|
||||
break;
|
||||
case 4:
|
||||
rgb->r = m + x;
|
||||
rgb->g = m;
|
||||
rgb->b = m + chroma;
|
||||
break;
|
||||
case 5:
|
||||
rgb->r = m + chroma;
|
||||
rgb->g = m;
|
||||
rgb->b = m + x;
|
||||
break;
|
||||
}
|
||||
switch ((uint8_t)a % 6) {
|
||||
case 0:
|
||||
rgb->r = m + chroma;
|
||||
rgb->g = m + x;
|
||||
rgb->b = m;
|
||||
break;
|
||||
case 1:
|
||||
rgb->r = m + x;
|
||||
rgb->g = m + chroma;
|
||||
rgb->b = m;
|
||||
break;
|
||||
case 2:
|
||||
rgb->r = m;
|
||||
rgb->g = m + chroma;
|
||||
rgb->b = m + x;
|
||||
break;
|
||||
case 3:
|
||||
rgb->r = m;
|
||||
rgb->g = m + x;
|
||||
rgb->b = m + chroma;
|
||||
break;
|
||||
case 4:
|
||||
rgb->r = m + x;
|
||||
rgb->g = m;
|
||||
rgb->b = m + chroma;
|
||||
break;
|
||||
case 5:
|
||||
rgb->r = m + chroma;
|
||||
rgb->g = m;
|
||||
rgb->b = m + x;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts ZMKs RGB (float) to Zephyr's led_rgb (uint8_t) format.
|
||||
*/
|
||||
void zmk_rgb_to_led_rgb(const struct zmk_color_rgb *rgb, struct led_rgb *led) {
|
||||
led->r = (uint8_t) (rgb->r * 255);
|
||||
led->g = (uint8_t) (rgb->g * 255);
|
||||
led->b = (uint8_t) (rgb->b * 255);
|
||||
led->r = rgb->r * 255;
|
||||
led->g = rgb->g * 255;
|
||||
led->b = rgb->b * 255;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two HSL colors.
|
||||
*/
|
||||
bool zmk_cmp_hsl(const struct zmk_color_hsl *a, const struct zmk_color_hsl *b) {
|
||||
return a->h == b->h && a->s == b->s && a->l == b->l;
|
||||
return a->h == b->h && a->s == b->s && a->l == b->l;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolate between two colors using the cylindrical model (HSL).
|
||||
*/
|
||||
void zmk_interpolate_hsl(const struct zmk_color_hsl *from, const struct zmk_color_hsl *to,
|
||||
struct zmk_color_hsl *result, float step) {
|
||||
struct zmk_color_hsl *result, float step) {
|
||||
int16_t hue_delta;
|
||||
|
||||
hue_delta = from->h - to->h;
|
||||
hue_delta = hue_delta + (180 < abs(hue_delta) ? (hue_delta < 0 ? 360 : -360) : 0);
|
||||
|
||||
result->h = (uint16_t) (360 + from->h - (hue_delta * step)) % 360;
|
||||
result->h = (uint16_t)(360 + from->h - (hue_delta * step)) % 360;
|
||||
result->s = from->s - (from->s - to->s) * step;
|
||||
result->l = from->l - (from->l - to->l) * step;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue