2015-07-30 20:14:55 +02:00
|
|
|
package juloo.keyboard2;
|
|
|
|
|
|
|
|
import android.content.Context;
|
2022-05-07 23:51:00 +02:00
|
|
|
import android.content.ContextWrapper;
|
2015-07-30 20:14:55 +02:00
|
|
|
import android.graphics.Canvas;
|
|
|
|
import android.graphics.Paint;
|
2015-09-30 22:47:18 +02:00
|
|
|
import android.graphics.RectF;
|
2022-05-07 23:51:00 +02:00
|
|
|
import android.inputmethodservice.InputMethodService;
|
|
|
|
import android.os.Build.VERSION;
|
2015-09-30 22:47:18 +02:00
|
|
|
import android.util.AttributeSet;
|
|
|
|
import android.util.DisplayMetrics;
|
2022-06-06 17:00:58 +02:00
|
|
|
import android.view.HapticFeedbackConstants;
|
2022-01-10 00:27:22 +01:00
|
|
|
import android.view.KeyEvent;
|
2015-07-30 20:14:55 +02:00
|
|
|
import android.view.MotionEvent;
|
|
|
|
import android.view.View;
|
2022-05-07 23:51:00 +02:00
|
|
|
import android.view.Window;
|
2015-07-30 20:14:55 +02:00
|
|
|
|
|
|
|
public class Keyboard2View extends View
|
2022-02-20 13:09:39 +01:00
|
|
|
implements View.OnTouchListener, Pointers.IPointerEventHandler
|
2015-07-30 20:14:55 +02:00
|
|
|
{
|
2021-12-19 19:44:27 +01:00
|
|
|
private KeyboardData _keyboard;
|
2022-09-24 22:36:06 +02:00
|
|
|
private KeyValue _shift_kv;
|
|
|
|
private KeyboardData.Key _shift_key;
|
2015-07-30 20:14:55 +02:00
|
|
|
|
2022-02-20 13:09:39 +01:00
|
|
|
private Pointers _pointers;
|
2015-08-01 23:54:38 +02:00
|
|
|
|
2022-06-05 01:38:42 +02:00
|
|
|
private Pointers.Modifiers _mods;
|
2015-08-01 16:33:30 +02:00
|
|
|
|
2021-12-19 19:44:27 +01:00
|
|
|
private static int _currentWhat = 0;
|
2015-08-05 01:30:56 +02:00
|
|
|
|
2021-12-19 19:44:27 +01:00
|
|
|
private Config _config;
|
2015-10-29 12:49:40 +01:00
|
|
|
|
2021-12-19 19:44:27 +01:00
|
|
|
private float _keyWidth;
|
2015-10-29 12:49:40 +01:00
|
|
|
|
2021-12-26 23:55:18 +01:00
|
|
|
private Theme _theme;
|
2015-10-29 12:49:40 +01:00
|
|
|
|
2021-12-19 19:44:27 +01:00
|
|
|
private static RectF _tmpRect = new RectF();
|
2015-07-30 20:14:55 +02:00
|
|
|
|
2022-02-02 21:46:23 +01:00
|
|
|
enum Vertical
|
|
|
|
{
|
|
|
|
TOP,
|
|
|
|
CENTER,
|
|
|
|
BOTTOM
|
|
|
|
}
|
|
|
|
|
2021-12-19 19:44:27 +01:00
|
|
|
public Keyboard2View(Context context, AttributeSet attrs)
|
|
|
|
{
|
|
|
|
super(context, attrs);
|
2021-12-26 23:55:18 +01:00
|
|
|
_theme = new Theme(getContext(), attrs);
|
2021-12-28 16:47:19 +01:00
|
|
|
_config = Config.globalConfig();
|
2022-02-20 13:09:39 +01:00
|
|
|
_pointers = new Pointers(this, _config);
|
2022-05-07 23:51:00 +02:00
|
|
|
refresh_navigation_bar(context);
|
2021-12-19 19:44:27 +01:00
|
|
|
setOnTouchListener(this);
|
2022-01-30 12:17:31 +01:00
|
|
|
reset();
|
2021-04-24 23:18:16 +02:00
|
|
|
}
|
|
|
|
|
2022-05-07 23:51:00 +02:00
|
|
|
private Window getParentWindow(Context context)
|
|
|
|
{
|
|
|
|
if (context instanceof InputMethodService)
|
|
|
|
return ((InputMethodService)context).getWindow().getWindow();
|
|
|
|
if (context instanceof ContextWrapper)
|
|
|
|
return getParentWindow(((ContextWrapper)context).getBaseContext());
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void refresh_navigation_bar(Context context)
|
|
|
|
{
|
|
|
|
if (VERSION.SDK_INT < 21)
|
|
|
|
return;
|
|
|
|
// The intermediate Window is a [Dialog].
|
|
|
|
Window w = getParentWindow(context);
|
|
|
|
int uiFlags = getSystemUiVisibility();
|
|
|
|
if (_theme.isLightNavBar)
|
|
|
|
uiFlags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
|
|
|
else
|
|
|
|
uiFlags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
|
|
|
w.setNavigationBarColor(_theme.colorNavBar);
|
|
|
|
setSystemUiVisibility(uiFlags);
|
|
|
|
}
|
|
|
|
|
2021-12-19 19:44:27 +01:00
|
|
|
public void setKeyboard(KeyboardData kw)
|
2021-05-09 00:09:10 +02:00
|
|
|
{
|
2022-03-05 18:15:36 +01:00
|
|
|
_keyboard = _config.modify_layout(kw);
|
2022-09-24 22:36:06 +02:00
|
|
|
_shift_kv = KeyValue.getKeyByName("shift");
|
|
|
|
_shift_key = _keyboard.findKeyWithValue(_shift_kv);
|
|
|
|
if (_shift_key == null)
|
|
|
|
{
|
|
|
|
_shift_kv = _shift_kv.withFlags(_shift_kv.getFlags() | KeyValue.FLAG_LOCK);
|
|
|
|
_shift_key = _keyboard.findKeyWithValue(_shift_kv);
|
|
|
|
}
|
2021-05-09 00:09:10 +02:00
|
|
|
reset();
|
|
|
|
}
|
2015-08-08 16:47:22 +02:00
|
|
|
|
2021-12-19 19:44:27 +01:00
|
|
|
public void reset()
|
|
|
|
{
|
2022-06-05 01:38:42 +02:00
|
|
|
_mods = Pointers.Modifiers.EMPTY;
|
2022-02-20 13:09:39 +01:00
|
|
|
_pointers.clear();
|
2021-12-19 19:44:27 +01:00
|
|
|
requestLayout();
|
|
|
|
invalidate();
|
|
|
|
}
|
|
|
|
|
2022-07-24 20:02:48 +02:00
|
|
|
/** Called by auto-capitalisation. */
|
|
|
|
public void set_shift_state(boolean state)
|
|
|
|
{
|
2022-09-24 22:36:06 +02:00
|
|
|
if (_keyboard == null || _shift_key == null)
|
2022-07-30 18:19:28 +02:00
|
|
|
return;
|
2022-09-24 22:36:06 +02:00
|
|
|
if (state)
|
|
|
|
_pointers.add_fake_pointer(_shift_kv, _shift_key);
|
|
|
|
else
|
|
|
|
_pointers.remove_fake_pointer(_shift_kv, _shift_key);
|
|
|
|
invalidate();
|
2022-07-24 20:02:48 +02:00
|
|
|
}
|
|
|
|
|
2022-06-05 01:38:42 +02:00
|
|
|
public KeyValue modifyKey(KeyValue k, Pointers.Modifiers mods)
|
2022-02-20 13:09:39 +01:00
|
|
|
{
|
2022-06-05 01:38:42 +02:00
|
|
|
return KeyModifier.modify(k, mods);
|
2022-02-20 13:09:39 +01:00
|
|
|
}
|
|
|
|
|
2022-05-08 16:53:33 +02:00
|
|
|
public void onPointerDown(boolean isSwipe)
|
2022-02-20 13:09:39 +01:00
|
|
|
{
|
2022-05-08 16:53:33 +02:00
|
|
|
invalidate();
|
|
|
|
vibrate();
|
2022-02-20 13:09:39 +01:00
|
|
|
}
|
|
|
|
|
2022-06-05 01:38:42 +02:00
|
|
|
public void onPointerUp(KeyValue k, Pointers.Modifiers mods)
|
2022-02-20 13:09:39 +01:00
|
|
|
{
|
2022-06-05 01:38:42 +02:00
|
|
|
_config.handler.handleKeyUp(k, mods);
|
2022-02-20 13:09:39 +01:00
|
|
|
invalidate();
|
|
|
|
}
|
|
|
|
|
2022-06-05 01:38:42 +02:00
|
|
|
public void onPointerHold(KeyValue k, Pointers.Modifiers mods)
|
2022-02-20 13:09:39 +01:00
|
|
|
{
|
2022-06-05 01:38:42 +02:00
|
|
|
_config.handler.handleKeyUp(k, mods);
|
2022-02-20 13:09:39 +01:00
|
|
|
}
|
|
|
|
|
2022-07-24 23:55:00 +02:00
|
|
|
public void onPointerFlagsChanged(boolean shouldVibrate)
|
2022-02-20 13:09:39 +01:00
|
|
|
{
|
|
|
|
invalidate();
|
2022-07-24 23:55:00 +02:00
|
|
|
if (shouldVibrate)
|
|
|
|
vibrate();
|
2022-02-20 13:09:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private void updateFlags()
|
|
|
|
{
|
2022-06-05 01:38:42 +02:00
|
|
|
_mods = _pointers.getModifiers();
|
2022-02-20 13:09:39 +01:00
|
|
|
}
|
|
|
|
|
2021-12-19 19:44:27 +01:00
|
|
|
@Override
|
|
|
|
public boolean onTouch(View v, MotionEvent event)
|
|
|
|
{
|
|
|
|
int p;
|
|
|
|
switch (event.getActionMasked())
|
|
|
|
{
|
|
|
|
case MotionEvent.ACTION_UP:
|
|
|
|
case MotionEvent.ACTION_POINTER_UP:
|
2022-02-20 13:09:39 +01:00
|
|
|
_pointers.onTouchUp(event.getPointerId(event.getActionIndex()));
|
2022-03-15 20:44:02 +01:00
|
|
|
break;
|
2021-12-19 19:44:27 +01:00
|
|
|
case MotionEvent.ACTION_DOWN:
|
|
|
|
case MotionEvent.ACTION_POINTER_DOWN:
|
|
|
|
p = event.getActionIndex();
|
2022-02-20 13:09:39 +01:00
|
|
|
float tx = event.getX(p);
|
|
|
|
float ty = event.getY(p);
|
|
|
|
KeyboardData.Key key = getKeyAtPosition(tx, ty);
|
|
|
|
if (key != null)
|
|
|
|
_pointers.onTouchDown(tx, ty, event.getPointerId(p), key);
|
2022-03-15 20:44:02 +01:00
|
|
|
break;
|
2021-12-19 19:44:27 +01:00
|
|
|
case MotionEvent.ACTION_MOVE:
|
|
|
|
for (p = 0; p < event.getPointerCount(); p++)
|
2022-02-20 13:09:39 +01:00
|
|
|
_pointers.onTouchMove(event.getX(p), event.getY(p), event.getPointerId(p));
|
2022-03-15 20:44:02 +01:00
|
|
|
break;
|
|
|
|
case MotionEvent.ACTION_CANCEL:
|
|
|
|
_pointers.onTouchCancel(event.getPointerId(event.getActionIndex()));
|
|
|
|
break;
|
2021-12-19 19:44:27 +01:00
|
|
|
default:
|
|
|
|
return (false);
|
|
|
|
}
|
|
|
|
return (true);
|
|
|
|
}
|
|
|
|
|
2022-02-20 13:09:39 +01:00
|
|
|
private KeyboardData.Row getRowAtPosition(float ty)
|
2021-12-19 19:44:27 +01:00
|
|
|
{
|
2022-02-20 13:09:39 +01:00
|
|
|
float y = _config.marginTop;
|
|
|
|
if (ty < y)
|
|
|
|
return null;
|
2021-12-19 19:44:27 +01:00
|
|
|
for (KeyboardData.Row row : _keyboard.rows)
|
|
|
|
{
|
2022-02-20 13:09:39 +01:00
|
|
|
y += (row.shift + row.height) * _config.keyHeight;
|
|
|
|
if (ty < y)
|
|
|
|
return row;
|
2021-12-19 19:44:27 +01:00
|
|
|
}
|
2022-02-20 13:09:39 +01:00
|
|
|
return null;
|
2021-04-29 00:08:55 +02:00
|
|
|
}
|
|
|
|
|
2022-02-20 13:09:39 +01:00
|
|
|
private KeyboardData.Key getKeyAtPosition(float tx, float ty)
|
2021-12-19 19:44:27 +01:00
|
|
|
{
|
2022-02-20 13:09:39 +01:00
|
|
|
KeyboardData.Row row = getRowAtPosition(ty);
|
|
|
|
float x = _config.horizontalMargin;
|
|
|
|
if (row == null || tx < x)
|
|
|
|
return null;
|
|
|
|
for (KeyboardData.Key key : row.keys)
|
2021-12-19 19:44:27 +01:00
|
|
|
{
|
2022-02-20 13:09:39 +01:00
|
|
|
x += (key.shift + key.width) * _keyWidth;
|
|
|
|
if (tx < x)
|
|
|
|
return key;
|
2021-12-19 19:44:27 +01:00
|
|
|
}
|
2022-02-20 13:09:39 +01:00
|
|
|
return null;
|
2021-12-19 19:44:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private void vibrate()
|
|
|
|
{
|
2022-06-24 22:00:23 +02:00
|
|
|
if (!_config.vibrateEnabled)
|
|
|
|
return ;
|
2022-06-06 20:37:22 +02:00
|
|
|
if (VERSION.SDK_INT >= 5)
|
2021-12-19 19:44:27 +01:00
|
|
|
{
|
2022-06-06 20:37:22 +02:00
|
|
|
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
|
|
|
|
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
|
2021-12-19 19:44:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onMeasure(int wSpec, int hSpec)
|
|
|
|
{
|
2021-04-29 00:59:19 +02:00
|
|
|
DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
|
2021-12-05 19:36:54 +01:00
|
|
|
int width = dm.widthPixels;
|
|
|
|
int height =
|
|
|
|
(int)(_config.keyHeight * _keyboard.keysHeight
|
2022-01-15 20:24:27 +01:00
|
|
|
+ _keyboard.rows.size()
|
2021-12-05 19:36:54 +01:00
|
|
|
+ _config.marginTop + _config.marginBottom);
|
|
|
|
setMeasuredDimension(width, height);
|
|
|
|
_keyWidth = (width - (_config.horizontalMargin * 2)) / _keyboard.keysWidth;
|
2021-12-19 19:44:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onDraw(Canvas canvas)
|
|
|
|
{
|
2022-03-19 15:39:20 +01:00
|
|
|
updateFlags();
|
2022-01-15 20:24:27 +01:00
|
|
|
float y = _config.marginTop + _config.keyVerticalInterval / 2;
|
2021-12-19 19:44:27 +01:00
|
|
|
for (KeyboardData.Row row : _keyboard.rows)
|
|
|
|
{
|
2021-04-29 00:59:19 +02:00
|
|
|
y += row.shift * _config.keyHeight;
|
2022-01-15 20:24:27 +01:00
|
|
|
float x = _config.horizontalMargin + _config.keyHorizontalInterval / 2;
|
|
|
|
float keyH = row.height * _config.keyHeight - _config.keyVerticalInterval;
|
2021-12-19 19:44:27 +01:00
|
|
|
for (KeyboardData.Key k : row.keys)
|
|
|
|
{
|
2022-01-15 20:24:27 +01:00
|
|
|
x += k.shift * _keyWidth;
|
2021-12-19 19:44:27 +01:00
|
|
|
float keyW = _keyWidth * k.width - _config.keyHorizontalInterval;
|
2022-02-20 13:09:39 +01:00
|
|
|
boolean isKeyDown = _pointers.isKeyDown(k);
|
2021-12-19 19:44:27 +01:00
|
|
|
_tmpRect.set(x, y, x + keyW, y + keyH);
|
2021-12-30 00:52:50 +01:00
|
|
|
canvas.drawRoundRect(_tmpRect, _theme.keyBorderRadius, _theme.keyBorderRadius,
|
2022-02-20 13:09:39 +01:00
|
|
|
isKeyDown ? _theme.keyDownBgPaint : _theme.keyBgPaint);
|
2022-03-13 00:35:15 +01:00
|
|
|
drawLabel(canvas, k.key0, keyW / 2f + x, y, keyH, isKeyDown);
|
2022-02-02 21:46:23 +01:00
|
|
|
if (k.edgekeys)
|
|
|
|
{
|
2022-03-13 00:35:15 +01:00
|
|
|
drawSubLabel(canvas, k.key1, x, y, keyW, keyH, Paint.Align.CENTER, Vertical.TOP, isKeyDown);
|
|
|
|
drawSubLabel(canvas, k.key3, x, y, keyW, keyH, Paint.Align.LEFT, Vertical.CENTER, isKeyDown);
|
|
|
|
drawSubLabel(canvas, k.key2, x, y, keyW, keyH, Paint.Align.RIGHT, Vertical.CENTER, isKeyDown);
|
|
|
|
drawSubLabel(canvas, k.key4, x, y, keyW, keyH, Paint.Align.CENTER, Vertical.BOTTOM, isKeyDown);
|
2022-02-02 21:46:23 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-03-13 00:35:15 +01:00
|
|
|
drawSubLabel(canvas, k.key1, x, y, keyW, keyH, Paint.Align.LEFT, Vertical.TOP, isKeyDown);
|
|
|
|
drawSubLabel(canvas, k.key3, x, y, keyW, keyH, Paint.Align.LEFT, Vertical.BOTTOM, isKeyDown);
|
|
|
|
drawSubLabel(canvas, k.key2, x, y, keyW, keyH, Paint.Align.RIGHT, Vertical.TOP, isKeyDown);
|
|
|
|
drawSubLabel(canvas, k.key4, x, y, keyW, keyH, Paint.Align.RIGHT, Vertical.BOTTOM, isKeyDown);
|
2022-02-02 21:46:23 +01:00
|
|
|
}
|
2022-01-15 20:24:27 +01:00
|
|
|
x += _keyWidth * k.width;
|
2021-12-19 19:44:27 +01:00
|
|
|
}
|
2022-01-15 20:24:27 +01:00
|
|
|
y += row.height * _config.keyHeight;
|
2021-12-19 19:44:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onDetachedFromWindow()
|
|
|
|
{
|
|
|
|
super.onDetachedFromWindow();
|
|
|
|
}
|
2015-10-28 00:39:20 +01:00
|
|
|
|
2022-02-20 13:09:39 +01:00
|
|
|
private int labelColor(KeyValue k, boolean isKeyDown, int defaultColor)
|
2021-04-29 01:33:57 +02:00
|
|
|
{
|
2022-06-05 17:26:34 +02:00
|
|
|
if (isKeyDown && k.hasFlags(KeyValue.FLAG_LATCH))
|
2021-04-29 01:33:57 +02:00
|
|
|
{
|
2022-02-20 13:09:39 +01:00
|
|
|
int flags = _pointers.getKeyFlags(k);
|
|
|
|
if (flags != -1)
|
2021-04-29 01:33:57 +02:00
|
|
|
{
|
2022-02-20 13:09:39 +01:00
|
|
|
if ((flags & KeyValue.FLAG_LOCKED) != 0)
|
2021-12-26 23:55:18 +01:00
|
|
|
return _theme.lockedColor;
|
2022-02-20 13:09:39 +01:00
|
|
|
if ((flags & KeyValue.FLAG_LATCH) == 0)
|
2021-12-26 23:55:18 +01:00
|
|
|
return _theme.activatedColor;
|
2021-04-29 01:33:57 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return defaultColor;
|
|
|
|
}
|
|
|
|
|
2022-06-24 20:26:27 +02:00
|
|
|
private void drawLabel(Canvas canvas, KeyboardData.Corner k, float x, float y, float keyH, boolean isKeyDown)
|
2021-12-19 19:44:27 +01:00
|
|
|
{
|
2022-03-13 00:35:15 +01:00
|
|
|
if (k == null)
|
|
|
|
return;
|
2022-06-24 20:26:27 +02:00
|
|
|
KeyValue kv = KeyModifier.modify(k.kv, _mods);
|
|
|
|
if (kv == null)
|
|
|
|
return;
|
|
|
|
float textSize = scaleTextSize(kv, _config.labelTextSize, keyH);
|
|
|
|
Paint p = _theme.labelPaint(kv.hasFlags(KeyValue.FLAG_KEY_FONT));
|
|
|
|
p.setColor(labelColor(kv, isKeyDown, _theme.labelColor));
|
2022-02-27 02:26:45 +01:00
|
|
|
p.setTextSize(textSize);
|
2022-06-24 20:26:27 +02:00
|
|
|
canvas.drawText(kv.getString(), x, (keyH - p.ascent() - p.descent()) / 2f + y, p);
|
2021-12-19 19:44:27 +01:00
|
|
|
}
|
2015-10-13 00:02:34 +02:00
|
|
|
|
2022-06-24 20:26:27 +02:00
|
|
|
private void drawSubLabel(Canvas canvas, KeyboardData.Corner k, float x, float y, float keyW, float keyH, Paint.Align a, Vertical v, boolean isKeyDown)
|
2021-12-19 19:44:27 +01:00
|
|
|
{
|
2022-03-13 00:35:15 +01:00
|
|
|
if (k == null)
|
|
|
|
return;
|
2022-06-24 20:26:27 +02:00
|
|
|
KeyValue kv = KeyModifier.modify(k.kv, _mods);
|
|
|
|
if (kv == null)
|
|
|
|
return;
|
|
|
|
float textSize = scaleTextSize(kv, _config.sublabelTextSize, keyH);
|
|
|
|
Paint p = _theme.subLabelPaint(kv.hasFlags(KeyValue.FLAG_KEY_FONT), a);
|
|
|
|
p.setColor(labelColor(kv, isKeyDown, _theme.subLabelColor));
|
2022-02-27 02:26:45 +01:00
|
|
|
p.setTextSize(textSize);
|
|
|
|
float subPadding = _config.keyPadding;
|
2022-02-02 21:46:23 +01:00
|
|
|
if (v == Vertical.CENTER)
|
2022-02-27 02:26:45 +01:00
|
|
|
y += (keyH - p.ascent() - p.descent()) / 2f;
|
|
|
|
else
|
|
|
|
y += (v == Vertical.TOP) ? subPadding - p.ascent() : keyH - subPadding - p.descent();
|
|
|
|
if (a == Paint.Align.CENTER)
|
|
|
|
x += keyW / 2f;
|
2022-02-02 21:46:23 +01:00
|
|
|
else
|
2022-02-27 02:26:45 +01:00
|
|
|
x += (a == Paint.Align.LEFT) ? subPadding : keyW - subPadding;
|
2022-06-24 20:26:27 +02:00
|
|
|
canvas.drawText(kv.getString(), x, y, p);
|
2021-12-19 19:44:27 +01:00
|
|
|
}
|
2015-10-13 00:02:34 +02:00
|
|
|
|
2022-02-27 02:26:45 +01:00
|
|
|
private float scaleTextSize(KeyValue k, float rel_size, float keyH)
|
2021-04-24 23:38:29 +02:00
|
|
|
{
|
2022-06-05 17:26:34 +02:00
|
|
|
float smaller_font = k.hasFlags(KeyValue.FLAG_SMALLER_FONT) ? 0.75f : 1.f;
|
2022-02-27 15:23:36 +01:00
|
|
|
return keyH * rel_size * smaller_font * _config.characterSize;
|
2021-04-24 23:38:29 +02:00
|
|
|
}
|
2015-07-30 20:14:55 +02:00
|
|
|
}
|