Initial commit
This commit is contained in:
commit
8e68e694fb
|
@ -0,0 +1,12 @@
|
||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
flask = "*"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.10"
|
|
@ -0,0 +1,107 @@
|
||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "295fa60b4ad3b19ec29744ec2dfafba79ad5ee9a0b9ff095ac626e3d3981f117"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.10"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
|
||||||
|
"sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==8.1.3"
|
||||||
|
},
|
||||||
|
"flask": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b",
|
||||||
|
"sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.2.2"
|
||||||
|
},
|
||||||
|
"itsdangerous": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44",
|
||||||
|
"sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==2.1.2"
|
||||||
|
},
|
||||||
|
"jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
|
||||||
|
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.1.2"
|
||||||
|
},
|
||||||
|
"markupsafe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003",
|
||||||
|
"sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88",
|
||||||
|
"sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5",
|
||||||
|
"sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7",
|
||||||
|
"sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a",
|
||||||
|
"sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603",
|
||||||
|
"sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1",
|
||||||
|
"sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135",
|
||||||
|
"sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247",
|
||||||
|
"sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6",
|
||||||
|
"sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601",
|
||||||
|
"sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77",
|
||||||
|
"sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02",
|
||||||
|
"sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e",
|
||||||
|
"sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63",
|
||||||
|
"sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f",
|
||||||
|
"sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980",
|
||||||
|
"sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b",
|
||||||
|
"sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812",
|
||||||
|
"sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff",
|
||||||
|
"sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96",
|
||||||
|
"sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1",
|
||||||
|
"sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925",
|
||||||
|
"sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a",
|
||||||
|
"sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6",
|
||||||
|
"sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e",
|
||||||
|
"sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f",
|
||||||
|
"sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4",
|
||||||
|
"sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f",
|
||||||
|
"sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3",
|
||||||
|
"sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c",
|
||||||
|
"sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a",
|
||||||
|
"sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417",
|
||||||
|
"sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a",
|
||||||
|
"sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a",
|
||||||
|
"sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37",
|
||||||
|
"sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452",
|
||||||
|
"sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933",
|
||||||
|
"sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a",
|
||||||
|
"sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==2.1.1"
|
||||||
|
},
|
||||||
|
"werkzeug": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f",
|
||||||
|
"sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==2.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
|
||||||
|
from flask import Flask, render_template, request
|
||||||
|
from os import system
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
#define actuators GPIOs
|
||||||
|
ledRed = 13
|
||||||
|
ledYlw = 19
|
||||||
|
ledGrn = 26
|
||||||
|
#initialize GPIO status variables
|
||||||
|
ledRedSts = 0
|
||||||
|
ledYlwSts = 0
|
||||||
|
ledGrnSts = 0
|
||||||
|
|
||||||
|
def check_app_states(apps):
|
||||||
|
for app in apps:
|
||||||
|
if 'check_cmd' in app:
|
||||||
|
exitcode = system(app['check_cmd'])
|
||||||
|
app['default'] = app['default'] if exitcode == 0 else False
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
if request.args.get('cmd') is not None:
|
||||||
|
exitcode = system(request.args.get('cmd'))
|
||||||
|
if exitcode == 0:
|
||||||
|
return "Command was run", 200
|
||||||
|
else:
|
||||||
|
return "Command failed (exit code {0})".format(exitcode), 500
|
||||||
|
|
||||||
|
templateData = {
|
||||||
|
'is_local': True if request.remote_addr == "127.0.0.1" else False,
|
||||||
|
'apps': [
|
||||||
|
{
|
||||||
|
'name': 'clock',
|
||||||
|
'cmd': 'espeak "$text"'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'toggle',
|
||||||
|
'text_on': 'ON',
|
||||||
|
'text_off': 'OFF',
|
||||||
|
'cmd_on': 'espeak "on"',
|
||||||
|
'cmd_off': 'espeak "off"',
|
||||||
|
'cooldown': 3000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'toggle',
|
||||||
|
'text_true': '🔊',
|
||||||
|
'text_false': '🔇',
|
||||||
|
'default': False,
|
||||||
|
'cmd_true': 'mpg123 ~/tng_viewscreen_on.mp3',
|
||||||
|
'cmd_false': 'mpg123 ~/tng_viewscreen_off.mp3',
|
||||||
|
'cooldown': 1000,
|
||||||
|
'show_cooldown': False
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'toggle',
|
||||||
|
'text_true': '🔊',
|
||||||
|
'text_false': '🔇',
|
||||||
|
'default': False,
|
||||||
|
'cmd_true': 'touch ~/testfile.tmp',
|
||||||
|
'cmd_false': 'rm ~/testfile.tmp',
|
||||||
|
'check_cmd': 'test -f ~/testfile.tmp',
|
||||||
|
'cooldown': 1000,
|
||||||
|
'show_cooldown': False
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
check_app_states(templateData)
|
||||||
|
|
||||||
|
return render_template('index.html', **templateData)
|
||||||
|
|
||||||
|
@app.route("/configure")
|
||||||
|
def configure():
|
||||||
|
return render_template('configure.html')
|
||||||
|
|
||||||
|
@app.route("/<deviceName>/<action>")
|
||||||
|
def action(deviceName, action):
|
||||||
|
templateData = {}
|
||||||
|
return render_template('index.html', **templateData)
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host='0.0.0.0', port=8081, debug=True)
|
|
@ -0,0 +1,102 @@
|
||||||
|
html
|
||||||
|
{
|
||||||
|
background:#111;
|
||||||
|
color:grey;
|
||||||
|
text-align:center;
|
||||||
|
font-family:sans-serif;
|
||||||
|
font-size:large;
|
||||||
|
line-height:1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul
|
||||||
|
{
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
select, button
|
||||||
|
{
|
||||||
|
padding:10px;
|
||||||
|
font:inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
select
|
||||||
|
{
|
||||||
|
min-width:200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button
|
||||||
|
{
|
||||||
|
padding:0.5em 1em 0.25em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
body
|
||||||
|
{
|
||||||
|
margin:auto;
|
||||||
|
max-width:600px;
|
||||||
|
margin-top:50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.cooldownProgress {
|
||||||
|
bottom:10px;
|
||||||
|
z-index:10;
|
||||||
|
height:3px;
|
||||||
|
background: #58A;
|
||||||
|
position:relative;
|
||||||
|
max-width:85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a
|
||||||
|
{
|
||||||
|
color:#58A;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.block.disabled
|
||||||
|
{
|
||||||
|
filter: grayscale(75%) brightness(60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.maximise {
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noselect {
|
||||||
|
-webkit-touch-callout: none; /* iOS Safari */
|
||||||
|
-webkit-user-select: none; /* Safari */
|
||||||
|
-khtml-user-select: none; /* Konqueror HTML */
|
||||||
|
-moz-user-select: none; /* Old versions of Firefox */
|
||||||
|
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||||
|
user-select: none; /* Non-prefixed version, currently
|
||||||
|
supported by Chrome, Edge, Opera and Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
#blocks
|
||||||
|
{
|
||||||
|
margin:auto;
|
||||||
|
max-width:350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.block
|
||||||
|
{
|
||||||
|
animation:filter;
|
||||||
|
display:inline-block;
|
||||||
|
width:100px;
|
||||||
|
height:100px;
|
||||||
|
line-height: 100px;
|
||||||
|
text-align:center;
|
||||||
|
vertical-align:middle;
|
||||||
|
border:solid 2px #58A;
|
||||||
|
color:#FFF;
|
||||||
|
border-radius:10px;
|
||||||
|
margin:6px 3px 6px 3px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a.block:hover
|
||||||
|
{
|
||||||
|
cursor:hand;
|
||||||
|
}
|
||||||
|
a.block:active
|
||||||
|
{
|
||||||
|
/*box-shadow: royalblue 0px 0px 8px;*/
|
||||||
|
opacity:70%;
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
function runCommand(cmd)
|
||||||
|
{
|
||||||
|
$.ajax({
|
||||||
|
url : './',
|
||||||
|
type : 'GET',
|
||||||
|
data : {
|
||||||
|
'cmd' : cmd
|
||||||
|
},
|
||||||
|
dataType:'json'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addApplet(config)
|
||||||
|
{
|
||||||
|
var fn = window["app_" + config['name']];
|
||||||
|
// is object a function?
|
||||||
|
if (typeof fn === "function")
|
||||||
|
{
|
||||||
|
//let cfg = JSON.parse(JSON.stringify(config));
|
||||||
|
let cfg = config
|
||||||
|
|
||||||
|
let a = $("<a>");
|
||||||
|
let obj = fn(a, cfg);
|
||||||
|
a.addClass("block");
|
||||||
|
a.append(obj);
|
||||||
|
$("#blocks").append(a);
|
||||||
|
|
||||||
|
if('cmd' in cfg)
|
||||||
|
{
|
||||||
|
a.click(function() {
|
||||||
|
command = cfg['cmd']
|
||||||
|
command = command.replace("$text", a.text());
|
||||||
|
runCommand(command)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('cooldown' in cfg)
|
||||||
|
{
|
||||||
|
let progressDiv = $("<div>");
|
||||||
|
progressDiv.addClass("cooldownProgress");
|
||||||
|
a.append(progressDiv);
|
||||||
|
progressDiv.hide();
|
||||||
|
|
||||||
|
a.click(function() {
|
||||||
|
a.css("pointer-events", "none");
|
||||||
|
a.addClass("disabled");
|
||||||
|
|
||||||
|
let cooldownTime = cfg['cooldown'];
|
||||||
|
let fadeInTime = 200;
|
||||||
|
|
||||||
|
if (cooldownTime < fadeInTime)
|
||||||
|
fadeInTime = cooldownTime*0.2;
|
||||||
|
|
||||||
|
let progBarTime = cooldownTime - fadeInTime;
|
||||||
|
|
||||||
|
progressDiv.css({
|
||||||
|
'width': '100%',
|
||||||
|
'margin-left': '8%'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cfg['show_cooldown'] !== false)
|
||||||
|
{
|
||||||
|
progressDiv.fadeIn(fadeInTime, function() {
|
||||||
|
progressDiv.animate({'width': '0%', 'margin-left':'50%'}, progBarTime);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
|
||||||
|
a.css("pointer-events", "auto");
|
||||||
|
a.removeClass("disabled");
|
||||||
|
progressDiv.hide();
|
||||||
|
}, cooldownTime);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
alert("unrecognised applet: " + cfg['name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_clock(a, config)
|
||||||
|
{
|
||||||
|
var span = $("<span>");
|
||||||
|
|
||||||
|
span.addClass("maximise");
|
||||||
|
|
||||||
|
function setTime() {
|
||||||
|
var today = new Date();
|
||||||
|
var h = today.getHours();
|
||||||
|
var m = today.getMinutes();
|
||||||
|
|
||||||
|
if (h < 10) h = "0" + h;
|
||||||
|
if (m < 10) m = "0" + m;
|
||||||
|
span.html(h + ":" + m);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(setTime, 1000);
|
||||||
|
setTime();
|
||||||
|
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_toggle(a, config)
|
||||||
|
{
|
||||||
|
var span = $("<span>");
|
||||||
|
|
||||||
|
span.addClass("maximise");
|
||||||
|
|
||||||
|
let currentState = false
|
||||||
|
let onIcon = "ON";
|
||||||
|
let offIcon = "OFF";
|
||||||
|
|
||||||
|
if ('default' in config) {
|
||||||
|
currentState = config['default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('text_on' in config) {
|
||||||
|
onIcon = config['text_on'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('text_off' in config) {
|
||||||
|
offIcon = config['text_off'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setToggleIcon() {
|
||||||
|
if (currentState == true) {
|
||||||
|
span.html(onIcon);
|
||||||
|
} else {
|
||||||
|
span.html(offIcon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.click(function() {
|
||||||
|
currentState = !currentState;
|
||||||
|
if (currentState == true && 'cmd_on' in config && config['cmd_on'].length > 0) {
|
||||||
|
runCommand(config['cmd_on'])
|
||||||
|
} else if (currentState == false && 'cmd_off' in config && config['cmd_off'].length > 0) {
|
||||||
|
runCommand(config['cmd_off'])
|
||||||
|
}
|
||||||
|
setToggleIcon();
|
||||||
|
});
|
||||||
|
|
||||||
|
setToggleIcon();
|
||||||
|
|
||||||
|
return span;
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>macropad (tbn)</title>
|
||||||
|
</head>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}" />
|
||||||
|
<body class="noselect">
|
||||||
|
<h2>Configure</h2>
|
||||||
|
|
||||||
|
Widgets:
|
||||||
|
<ul>
|
||||||
|
<li>app1 - ↑ / ↓ / <span style="color:red">X</span></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<select>
|
||||||
|
<option>option1</option>
|
||||||
|
<option>option2</option>
|
||||||
|
<option>option3</option>
|
||||||
|
</select>
|
||||||
|
<button>Add</button>
|
||||||
|
<hr>
|
||||||
|
<button style="font-weight:bold">Save changes</button>
|
||||||
|
or <a href="/">discard</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>macropad (tbn)</title>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.1.js"></script>
|
||||||
|
</head>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}" />
|
||||||
|
<body class="noselect">
|
||||||
|
{% if is_local %}
|
||||||
|
<a href="/configure" style="margin:20px; display:inline-block">Configure</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="blocks">
|
||||||
|
<!--a class="block">{{ time }}</a-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='widgets.js') }}"></script>
|
||||||
|
|
||||||
|
{% for app in apps %}
|
||||||
|
<script>
|
||||||
|
addApplet({{ app|tojson }})
|
||||||
|
</script>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue