Merge pull request #755 from mikeller/added_release_checking
Added checking for latest configurator release and update notification.10.3.x-maintenance 10.0.0-rc3
commit
7252c30c18
|
@ -5,12 +5,12 @@
|
|||
"warningTitle": {
|
||||
"message": "Warning"
|
||||
},
|
||||
"noticeTitle": {
|
||||
"message": "Notice"
|
||||
},
|
||||
"options_title": {
|
||||
"message": "Application Options"
|
||||
},
|
||||
"options_receive_app_notifications": {
|
||||
"message": "Receive desktop <strong>notification</strong> when application updates"
|
||||
},
|
||||
"connect": {
|
||||
"message": "Connect"
|
||||
},
|
||||
|
@ -35,6 +35,12 @@
|
|||
"permanentExpertMode": {
|
||||
"message": "Permanently enable Expert Mode"
|
||||
},
|
||||
"checkForConfiguratorUnstableVersions": {
|
||||
"message": "Show update notifications for unstable versions of the configurator"
|
||||
},
|
||||
"configuratorUpdateNotice": {
|
||||
"message": "You are using an outdated version of the <b>Betaflight Configurator</b>.<br>Version <b>$1</b> is available online, please visit <a href=\"$2\" target=\"_blank\">the release page</a> to download and install the latest version with fixes and improvements.<br>Please close the configurator window before updating."
|
||||
},
|
||||
"deviceRebooting": {
|
||||
"message": "Device - <span style=\"color: red\">Rebooting</span>"
|
||||
},
|
||||
|
|
26
eventPage.js
26
eventPage.js
|
@ -84,22 +84,18 @@ chrome.runtime.onInstalled.addListener(function (details) {
|
|||
|
||||
// only fire up notification sequence when one of the major version numbers changed
|
||||
if (currentVersionArr[0] > previousVersionArr[0] || currentVersionArr[1] > previousVersionArr[1]) {
|
||||
chrome.storage.local.get('update_notify', function (result) {
|
||||
if (result.update_notify === 'undefined' || result.update_notify) {
|
||||
var manifest = chrome.runtime.getManifest();
|
||||
var options = {
|
||||
priority: 0,
|
||||
type: 'basic',
|
||||
title: manifest.name,
|
||||
message: chrome.i18n.getMessage('notifications_app_just_updated_to_version', [getManifestVersion(manifest)]),
|
||||
iconUrl: '/images/icon_128.png',
|
||||
buttons: [{'title': chrome.i18n.getMessage('notifications_click_here_to_start_app')}]
|
||||
};
|
||||
var manifest = chrome.runtime.getManifest();
|
||||
var options = {
|
||||
priority: 0,
|
||||
type: 'basic',
|
||||
title: manifest.name,
|
||||
message: chrome.i18n.getMessage('notifications_app_just_updated_to_version', [getManifestVersion(manifest)]),
|
||||
iconUrl: '/images/icon_128.png',
|
||||
buttons: [{'title': chrome.i18n.getMessage('notifications_click_here_to_start_app')}]
|
||||
};
|
||||
|
||||
chrome.notifications.create('baseflight_update', options, function (notificationId) {
|
||||
// empty
|
||||
});
|
||||
}
|
||||
chrome.notifications.create('baseflight_update', options, function (notificationId) {
|
||||
// empty
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -202,6 +202,7 @@ gulp.task('dist', ['clean-dist'], function () {
|
|||
'./js/RateCurve.js',
|
||||
'./js/Features.js',
|
||||
'./js/Beepers.js',
|
||||
'./js/release_checker.js',
|
||||
'./tabs/adjustments.js',
|
||||
'./tabs/auxiliary.js',
|
||||
'./tabs/cli.js',
|
||||
|
|
|
@ -325,8 +325,7 @@ GUI_control.prototype.show_modal = function (title, message) {
|
|||
var popup = new jBox('Modal', {
|
||||
title: title,
|
||||
content: message,
|
||||
closeButton: 'title',
|
||||
closeOnClick: 'box'
|
||||
closeButton: 'title'
|
||||
});
|
||||
|
||||
popup.open();
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
'use strict;'
|
||||
|
||||
var ReleaseChecker = function (releaseName, releaseUrl) {
|
||||
var self = this;
|
||||
|
||||
self._releaseName = releaseName;
|
||||
self._releaseDataTag = `${self._releaseName}ReleaseData`;
|
||||
self._releaseLastUpdateTag = `${self._releaseName}ReleaseLastUpdate`
|
||||
self._releaseUrl = releaseUrl;
|
||||
|
||||
|
||||
}
|
||||
|
||||
ReleaseChecker.prototype.loadReleaseData = function (processFunction) {
|
||||
var self = this;
|
||||
chrome.storage.local.get([self._releaseLastUpdateTag, self._releaseDataTag], function (result) {
|
||||
var releaseDataTimestamp = $.now();
|
||||
var cacheReleaseData = result[self._releaseDataTag];
|
||||
var cachedReleaseLastUpdate = result[self._releaseLastUpdateTag];
|
||||
if (!cacheReleaseData || !cachedReleaseLastUpdate || releaseDataTimestamp - cachedReleaseLastUpdate > 3600 * 1000) {
|
||||
$.get(self._releaseUrl, function (releaseData) {
|
||||
GUI.log(`Loaded release information for ${self._releaseName} from GitHub.`);
|
||||
|
||||
var data = {};
|
||||
data[self._releaseDataTag] = releaseData
|
||||
data[self._releaseLastUpdateTag] = releaseDataTimestamp
|
||||
chrome.storage.local.set(data, function () {});
|
||||
|
||||
self._processReleaseData(releaseData, processFunction);
|
||||
}).fail(function (data) {
|
||||
var message = '';
|
||||
if (data['responseJSON']) {
|
||||
message = data['responseJSON'].message;
|
||||
}
|
||||
GUI.log(`<b>GitHub query for ${self._releaseName} releases failed, using cached information. Reason: <code>${message}</code></b>`);
|
||||
|
||||
self._processReleaseData(cacheReleaseData, processFunction);
|
||||
});
|
||||
} else {
|
||||
if (cacheReleaseData) {
|
||||
GUI.log(`Using cached release information for ${self._releaseName} releases.`);
|
||||
}
|
||||
|
||||
self._processReleaseData(cacheReleaseData, processFunction);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
ReleaseChecker.prototype._processReleaseData = function (releaseData, processFunction) {
|
||||
if (releaseData) {
|
||||
processFunction(releaseData);
|
||||
} else {
|
||||
GUI.log(`No release information available for ${self._releaseName}.`);
|
||||
|
||||
processFunction();
|
||||
}
|
||||
}
|
|
@ -73,6 +73,7 @@
|
|||
<script type="text/javascript" src="./js/RateCurve.js"></script>
|
||||
<script type="text/javascript" src="./js/Features.js"></script>
|
||||
<script type="text/javascript" src="./js/Beepers.js"></script>
|
||||
<script type="text/javascript" src="./js/release_checker.js"></script>
|
||||
<script type="text/javascript" src="./main.js"></script>
|
||||
<script type="text/javascript" src="./tabs/landing.js"></script>
|
||||
<script type="text/javascript" src="./tabs/setup.js"></script>
|
||||
|
|
92
main.js
92
main.js
|
@ -28,35 +28,7 @@ $(document).ready(function () {
|
|||
break;
|
||||
}
|
||||
|
||||
// check for newer releases online to inform people in case they are running an old release
|
||||
|
||||
chrome.storage.local.get(['lastVersionChecked', 'lastVersionAvailableOnline'], function (result) {
|
||||
if (typeof result.lastVersionChecked === undefined || ($.now() - result.lastVersionChecked) > 3600 * 1000) {
|
||||
try {
|
||||
var url = 'https://api.github.com/repos/betaflight/betaflight-configurator/tags';
|
||||
$.get(url).done(function (data) {
|
||||
var versions = data.sort(function (v1, v2) {
|
||||
try {
|
||||
return semver.compare(v2.name, v1.name);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
chrome.storage.local.set({
|
||||
'lastVersionChecked': $.now(),
|
||||
'lastVersionAvailableOnline': versions[0].name
|
||||
}, function (result) {
|
||||
console.log("Latest version available online: " + versions[0].name);
|
||||
});
|
||||
notifyOutdatedVersion(versions[0].name);
|
||||
});
|
||||
} catch (e) {
|
||||
// Just to catch and supress warnings if no internet connection is available
|
||||
}
|
||||
} else if (result.lastVersionAvailableOnline) {
|
||||
notifyOutdatedVersion(result.lastVersionAvailableOnline);
|
||||
}
|
||||
});
|
||||
checkForConfiguratorUpdates();
|
||||
|
||||
chrome.storage.local.get('logopen', function (result) {
|
||||
if (result.logopen) {
|
||||
|
@ -212,19 +184,6 @@ $(document).ready(function () {
|
|||
// translate to user-selected language
|
||||
localize();
|
||||
|
||||
// if notifications are enabled, or wasn't set, check the notifications checkbox
|
||||
chrome.storage.local.get('update_notify', function (result) {
|
||||
if (typeof result.update_notify === 'undefined' || result.update_notify) {
|
||||
$('div.notifications input').prop('checked', true);
|
||||
}
|
||||
|
||||
$('div.notifications input').change(function () {
|
||||
var check = $(this).is(':checked');
|
||||
|
||||
chrome.storage.local.set({'update_notify': check});
|
||||
});
|
||||
});
|
||||
|
||||
chrome.storage.local.get('permanentExpertMode', function (result) {
|
||||
if (result.permanentExpertMode) {
|
||||
$('div.permanentExpertMode input').prop('checked', true);
|
||||
|
@ -243,6 +202,21 @@ $(document).ready(function () {
|
|||
}).change();
|
||||
});
|
||||
|
||||
chrome.storage.local.get('checkForConfiguratorUnstableVersions', function (result) {
|
||||
$('div.checkForConfiguratorUnstableVersions input').change(function () {
|
||||
var checked = $(this).is(':checked');
|
||||
|
||||
chrome.storage.local.set({'checkForConfiguratorUnstableVersions': checked});
|
||||
|
||||
$('input[name="checkForConfiguratorUnstableVersions"]').prop('checked', checked).change();
|
||||
checkForConfiguratorUpdates();
|
||||
});
|
||||
|
||||
if (result.checkForConfiguratorUnstableVersions) {
|
||||
$('div.checkForConfiguratorUnstableVersions input').prop('checked', true);
|
||||
}
|
||||
});
|
||||
|
||||
function close_and_cleanup(e) {
|
||||
if (e.type == 'click' && !$.contains($('div#options-window')[0], e.target) || e.type == 'keyup' && e.keyCode == 27) {
|
||||
$(document).unbind('click keyup', close_and_cleanup);
|
||||
|
@ -377,10 +351,36 @@ $(document).ready(function () {
|
|||
});
|
||||
});
|
||||
|
||||
function notifyOutdatedVersion(version) {
|
||||
if (semver.lt(getManifestVersion(), version)) {
|
||||
GUI.log('You are using an old version of ' + chrome.runtime.getManifest().name + '. Version ' + version + ' is available online with possible improvements and fixes.');
|
||||
}
|
||||
function checkForConfiguratorUpdates() {
|
||||
var releaseChecker = new ReleaseChecker('configurator', 'https://api.github.com/repos/betaflight/betaflight-configurator/releases');
|
||||
|
||||
releaseChecker.loadReleaseData(notifyOutdatedVersion);
|
||||
}
|
||||
|
||||
function notifyOutdatedVersion(releaseData) {
|
||||
chrome.storage.local.get('checkForConfiguratorUnstableVersions', function (result) {
|
||||
var showUnstableReleases = false;
|
||||
if (result.checkForConfiguratorUnstableVersions) {
|
||||
showUnstableReleases = true;
|
||||
}
|
||||
var versions = releaseData.filter(function (version) {
|
||||
var semVerVersion = semver.parse(version.tag_name);
|
||||
if (semVerVersion && (showUnstableReleases || semVerVersion.prerelease.length === 0)) {
|
||||
return version;
|
||||
}
|
||||
}).sort(function (v1, v2) {
|
||||
try {
|
||||
return semver.compare(v2.tag_name, v1.tag_name);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (versions.length > 0 && semver.lt(getManifestVersion(), versions[0].tag_name)) {
|
||||
GUI.show_modal(chrome.i18n.getMessage('noticeTitle'), chrome.i18n.getMessage('configuratorUpdateNotice', [versions[0].tag_name, versions[0].html_url]));
|
||||
GUI.log(chrome.i18n.getMessage('configuratorUpdateNotice', [versions[0].tag_name, versions[0].html_url]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function update_packet_error(caller) {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
TABS.firmware_flasher = {
|
||||
releases: null,
|
||||
releaseChecker: new ReleaseChecker('firmware', 'https://api.github.com/repos/betaflight/betaflight/releases')
|
||||
};
|
||||
|
||||
TABS.firmware_flasher.initialize = function (callback) {
|
||||
|
@ -30,144 +31,108 @@ TABS.firmware_flasher.initialize = function (callback) {
|
|||
}
|
||||
|
||||
function buildBoardOptions(releaseData) {
|
||||
var boards_e = $('select[name="board"]').empty();
|
||||
var showDevReleases = ($('input.show_development_releases').is(':checked'));
|
||||
boards_e.append($("<option value='0'>{0}</option>".format(chrome.i18n.getMessage('firmwareFlasherOptionLabelSelectBoard'))));
|
||||
|
||||
var versions_e = $('select[name="firmware_version"]').empty();
|
||||
versions_e.append($("<option value='0'>{0}</option>".format(chrome.i18n.getMessage('firmwareFlasherOptionLabelSelectFirmwareVersion'))));
|
||||
|
||||
var releases = {};
|
||||
var sortedTargets = [];
|
||||
var unsortedTargets = [];
|
||||
releaseData.forEach(function(release){
|
||||
release.assets.forEach(function(asset){
|
||||
var targetFromFilenameExpression = /betaflight_([\d.]+)?_?(\w+)(\-.*)?\.(.*)/;
|
||||
var match = targetFromFilenameExpression.exec(asset.name);
|
||||
|
||||
if ((!showDevReleases && release.prerelease) || !match) {
|
||||
return;
|
||||
}
|
||||
var target = match[2];
|
||||
if($.inArray(target, unsortedTargets) == -1) {
|
||||
unsortedTargets.push(target);
|
||||
}
|
||||
});
|
||||
sortedTargets = unsortedTargets.sort();
|
||||
});
|
||||
sortedTargets.forEach(function(release) {
|
||||
releases[release] = [];
|
||||
});
|
||||
|
||||
releaseData.forEach(function(release){
|
||||
var versionFromTagExpression = /v?(.*)/;
|
||||
var matchVersionFromTag = versionFromTagExpression.exec(release.tag_name);
|
||||
var version = matchVersionFromTag[1];
|
||||
|
||||
release.assets.forEach(function(asset){
|
||||
var targetFromFilenameExpression = /betaflight_([\d.]+)?_?(\w+)(\-.*)?\.(.*)/;
|
||||
var match = targetFromFilenameExpression.exec(asset.name);
|
||||
|
||||
if ((!showDevReleases && release.prerelease) || !match) {
|
||||
return;
|
||||
}
|
||||
|
||||
var target = match[2];
|
||||
var format = match[4];
|
||||
|
||||
if (format != 'hex') {
|
||||
return;
|
||||
}
|
||||
|
||||
var date = new Date(release.published_at);
|
||||
var formattedDate = ("0" + date.getDate()).slice(-2) + "-" + ("0"+(date.getMonth()+1)).slice(-2) + "-" +
|
||||
date.getFullYear() + " " + ("0" + date.getHours()).slice(-2) + ":" + ("0" + date.getMinutes()).slice(-2);
|
||||
|
||||
var descriptor = {
|
||||
"releaseUrl": release.html_url,
|
||||
"name" : version,
|
||||
"version" : version,
|
||||
"url" : asset.browser_download_url,
|
||||
"file" : asset.name,
|
||||
"target" : target,
|
||||
"date" : formattedDate,
|
||||
"notes" : release.body,
|
||||
"status" : release.prerelease ? "release-candidate" : "stable"
|
||||
};
|
||||
releases[target].push(descriptor);
|
||||
});
|
||||
});
|
||||
var selectTargets = [];
|
||||
Object.keys(releases)
|
||||
.sort()
|
||||
.forEach(function(target, i) {
|
||||
var descriptors = releases[target];
|
||||
descriptors.forEach(function(descriptor){
|
||||
if($.inArray(target, selectTargets) == -1) {
|
||||
selectTargets.push(target);
|
||||
var select_e =
|
||||
$("<option value='{0}'>{0}</option>".format(
|
||||
descriptor.target
|
||||
)).data('summary', descriptor);
|
||||
boards_e.append(select_e);
|
||||
}
|
||||
});
|
||||
});
|
||||
TABS.firmware_flasher.releases = releases;
|
||||
chrome.storage.local.get('selected_board', function (result) {
|
||||
if (result.selected_board) {
|
||||
$('select[name="board"]').val(result.selected_board);
|
||||
$('select[name="board"]').trigger("change");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function loadReleaseData() {
|
||||
chrome.storage.local.get(['releaseDataLastUpdate', 'releaseData'], function (result) {
|
||||
var releaseDataTimestamp = $.now();
|
||||
if (!result.releaseData || !result.releaseDataLastUpdate || releaseDataTimestamp - result.releaseDataLastUpdate > 3600 * 1000) {
|
||||
$.get('https://api.github.com/repos/betaflight/betaflight/releases', function (releaseData) {
|
||||
GUI.log("Loaded release information from GitHub.");
|
||||
|
||||
chrome.storage.local.set({'releaseDataLastUpdate': releaseDataTimestamp, 'releaseData': releaseData}, function () {});
|
||||
|
||||
processReleaseData(releaseData);
|
||||
}).fail(function (data){
|
||||
processReleaseData(result.releaseData);
|
||||
|
||||
var message = '';
|
||||
if (data["responseJSON"]) {
|
||||
message = data["responseJSON"].message;
|
||||
}
|
||||
GUI.log("<b>GitHub query failed: <code>{0}</code></b>".format(message));
|
||||
});
|
||||
} else {
|
||||
if (result.releaseData) {
|
||||
GUI.log("Using cached release information.");
|
||||
}
|
||||
|
||||
processReleaseData(result.releaseData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function processReleaseData(releaseData) {
|
||||
if (releaseData) {
|
||||
buildBoardOptions(releaseData);
|
||||
} else {
|
||||
GUI.log("No release information available.");
|
||||
|
||||
if (!releaseData) {
|
||||
$('select[name="board"]').empty().append('<option value="0">Offline</option>');
|
||||
$('select[name="firmware_version"]').empty().append('<option value="0">Offline</option>');
|
||||
} else {
|
||||
var boards_e = $('select[name="board"]').empty();
|
||||
var showDevReleases = ($('input.show_development_releases').is(':checked'));
|
||||
boards_e.append($("<option value='0'>{0}</option>".format(chrome.i18n.getMessage('firmwareFlasherOptionLabelSelectBoard'))));
|
||||
|
||||
var versions_e = $('select[name="firmware_version"]').empty();
|
||||
versions_e.append($("<option value='0'>{0}</option>".format(chrome.i18n.getMessage('firmwareFlasherOptionLabelSelectFirmwareVersion'))));
|
||||
|
||||
var releases = {};
|
||||
var sortedTargets = [];
|
||||
var unsortedTargets = [];
|
||||
releaseData.forEach(function(release){
|
||||
release.assets.forEach(function(asset){
|
||||
var targetFromFilenameExpression = /betaflight_([\d.]+)?_?(\w+)(\-.*)?\.(.*)/;
|
||||
var match = targetFromFilenameExpression.exec(asset.name);
|
||||
|
||||
if ((!showDevReleases && release.prerelease) || !match) {
|
||||
return;
|
||||
}
|
||||
var target = match[2];
|
||||
if($.inArray(target, unsortedTargets) == -1) {
|
||||
unsortedTargets.push(target);
|
||||
}
|
||||
});
|
||||
sortedTargets = unsortedTargets.sort();
|
||||
});
|
||||
sortedTargets.forEach(function(release) {
|
||||
releases[release] = [];
|
||||
});
|
||||
|
||||
releaseData.forEach(function(release){
|
||||
var versionFromTagExpression = /v?(.*)/;
|
||||
var matchVersionFromTag = versionFromTagExpression.exec(release.tag_name);
|
||||
var version = matchVersionFromTag[1];
|
||||
|
||||
release.assets.forEach(function(asset){
|
||||
var targetFromFilenameExpression = /betaflight_([\d.]+)?_?(\w+)(\-.*)?\.(.*)/;
|
||||
var match = targetFromFilenameExpression.exec(asset.name);
|
||||
|
||||
if ((!showDevReleases && release.prerelease) || !match) {
|
||||
return;
|
||||
}
|
||||
|
||||
var target = match[2];
|
||||
var format = match[4];
|
||||
|
||||
if (format != 'hex') {
|
||||
return;
|
||||
}
|
||||
|
||||
var date = new Date(release.published_at);
|
||||
var formattedDate = ("0" + date.getDate()).slice(-2) + "-" + ("0"+(date.getMonth()+1)).slice(-2) + "-" + date.getFullYear() + " " + ("0" + date.getHours()).slice(-2) + ":" + ("0" + date.getMinutes()).slice(-2);
|
||||
|
||||
var descriptor = {
|
||||
"releaseUrl": release.html_url,
|
||||
"name" : version,
|
||||
"version" : version,
|
||||
"url" : asset.browser_download_url,
|
||||
"file" : asset.name,
|
||||
"target" : target,
|
||||
"date" : formattedDate,
|
||||
"notes" : release.body,
|
||||
"status" : release.prerelease ? "release-candidate" : "stable"
|
||||
};
|
||||
releases[target].push(descriptor);
|
||||
});
|
||||
});
|
||||
var selectTargets = [];
|
||||
Object.keys(releases)
|
||||
.sort()
|
||||
.forEach(function(target, i) {
|
||||
var descriptors = releases[target];
|
||||
descriptors.forEach(function(descriptor){
|
||||
if($.inArray(target, selectTargets) == -1) {
|
||||
selectTargets.push(target);
|
||||
var select_e =
|
||||
$("<option value='{0}'>{0}</option>".format(
|
||||
descriptor.target
|
||||
)).data('summary', descriptor);
|
||||
boards_e.append(select_e);
|
||||
}
|
||||
});
|
||||
});
|
||||
TABS.firmware_flasher.releases = releases;
|
||||
chrome.storage.local.get('selected_board', function (result) {
|
||||
if (result.selected_board) {
|
||||
$('select[name="board"]').val(result.selected_board);
|
||||
$('select[name="board"]').trigger("change");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// translate to user-selected language
|
||||
localize();
|
||||
|
||||
// bind events
|
||||
$('input.show_development_releases').click(function () {
|
||||
loadReleaseData();
|
||||
self.releaseChecker.loadReleaseData(buildBoardOptions);
|
||||
});
|
||||
|
||||
$('select[name="board"]').change(function() {
|
||||
|
@ -518,7 +483,7 @@ TABS.firmware_flasher.initialize = function (callback) {
|
|||
$('input.show_development_releases').prop('checked', false);
|
||||
}
|
||||
|
||||
loadReleaseData();
|
||||
self.releaseChecker.loadReleaseData(buildBoardOptions);
|
||||
|
||||
$('input.show_development_releases').change(function () {
|
||||
chrome.storage.local.set({'show_development_releases': $(this).is(':checked')});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="notifications">
|
||||
<label><input type="checkbox" /><span i18n="options_receive_app_notifications"></span></label>
|
||||
</div>
|
||||
<div class="permanentExpertMode">
|
||||
<label><input type="checkbox" /><span i18n="permanentExpertMode"></span></label>
|
||||
</div>
|
||||
<div class="checkForConfiguratorUnstableVersions">
|
||||
<label><input type="checkbox" /><span i18n="checkForConfiguratorUnstableVersions"></span></label>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue