mirror of
https://github.com/grocy/grocy.git
synced 2025-08-20 12:20:22 +00:00
Implemented "Scan mode"
This commit is contained in:
@@ -4,6 +4,14 @@
|
|||||||
- From there you can also edit the stock entries
|
- From there you can also edit the stock entries
|
||||||
- A huge THANK YOU goes to @kriddles for the work on this feature
|
- A huge THANK YOU goes to @kriddles for the work on this feature
|
||||||
|
|
||||||
|
### New feature: Scan mode
|
||||||
|
- New switch-button on the purchase and consume page
|
||||||
|
- When enabled
|
||||||
|
- The amount will always be filled with `1` after changing/scanning a product
|
||||||
|
- If all fields could be automatically populated (means for purchase the product has a default best before date set), the transaction is automatically submitted
|
||||||
|
- If not, a warning is displayed and you can fill in the missing information
|
||||||
|
- Audio feedback is provided after scanning and on success/error of the transaction
|
||||||
|
|
||||||
### New feature: Self produced products
|
### New feature: Self produced products
|
||||||
- To a recipe a product can be attached
|
- To a recipe a product can be attached
|
||||||
- This products needs a "Default best before date"
|
- This products needs a "Default best before date"
|
||||||
|
@@ -91,6 +91,8 @@ DefaultUserSetting('product_presets_qu_id', -1); // Default quantity unit id for
|
|||||||
DefaultUserSetting('stock_expring_soon_days', 5);
|
DefaultUserSetting('stock_expring_soon_days', 5);
|
||||||
DefaultUserSetting('stock_default_purchase_amount', 0);
|
DefaultUserSetting('stock_default_purchase_amount', 0);
|
||||||
DefaultUserSetting('stock_default_consume_amount', 1);
|
DefaultUserSetting('stock_default_consume_amount', 1);
|
||||||
|
DefaultUserSetting('scan_mode_consume_enabled', false);
|
||||||
|
DefaultUserSetting('scan_mode_purchase_enabled', false);
|
||||||
|
|
||||||
# Chores settings
|
# Chores settings
|
||||||
DefaultUserSetting('chores_due_soon_days', 5);
|
DefaultUserSetting('chores_due_soon_days', 5);
|
||||||
|
@@ -1669,3 +1669,15 @@ msgstr ""
|
|||||||
|
|
||||||
msgid "Meal plan product"
|
msgid "Meal plan product"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Scan mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "on"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "off"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Scan mode is on but not all required fields could be populated automatically"
|
||||||
|
msgstr ""
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
"bootbox": "^5.3.2",
|
"bootbox": "^5.3.2",
|
||||||
"bootstrap": "^4.3.1",
|
"bootstrap": "^4.3.1",
|
||||||
"bootstrap-select": "^1.13.10",
|
"bootstrap-select": "^1.13.10",
|
||||||
|
"bootstrap-switch-button": "https://github.com/walidbagh/bootstrap-switch-button#Fix-module-export",
|
||||||
"chart.js": "^2.8.0",
|
"chart.js": "^2.8.0",
|
||||||
"datatables.net": "^1.10.19",
|
"datatables.net": "^1.10.19",
|
||||||
"datatables.net-bs4": "^1.10.19",
|
"datatables.net-bs4": "^1.10.19",
|
||||||
|
@@ -425,7 +425,7 @@ $(document).on("click", "select", function()
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Auto saving user setting controls
|
// Auto saving user setting controls
|
||||||
$(".user-setting-control").on("change", function()
|
$(document).on("change", ".user-setting-control", function()
|
||||||
{
|
{
|
||||||
var element = $(this);
|
var element = $(this);
|
||||||
var settingKey = element.attr("data-setting-key");
|
var settingKey = element.attr("data-setting-key");
|
||||||
|
26
public/js/grocy_uisound.js
Normal file
26
public/js/grocy_uisound.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
Grocy.UISound = { };
|
||||||
|
|
||||||
|
Grocy.UISound.Play = function(url)
|
||||||
|
{
|
||||||
|
new Audio(url).play();
|
||||||
|
}
|
||||||
|
|
||||||
|
Grocy.UISound.AskForPermission = function()
|
||||||
|
{
|
||||||
|
Grocy.UISound.Play(U("/uisounds/silence.mp3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Grocy.UISound.Success = function()
|
||||||
|
{
|
||||||
|
Grocy.UISound.Play(U("/uisounds/success.mp3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Grocy.UISound.Error = function()
|
||||||
|
{
|
||||||
|
Grocy.UISound.Play(U("/uisounds/error.mp3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Grocy.UISound.BarcodeScannerBeep = function()
|
||||||
|
{
|
||||||
|
Grocy.UISound.Play(U("/uisounds/barcodescannerbeep.mp3"));
|
||||||
|
}
|
BIN
public/uisounds/barcodescannerbeep.mp3
Normal file
BIN
public/uisounds/barcodescannerbeep.mp3
Normal file
Binary file not shown.
BIN
public/uisounds/error.mp3
Normal file
BIN
public/uisounds/error.mp3
Normal file
Binary file not shown.
BIN
public/uisounds/silence.mp3
Normal file
BIN
public/uisounds/silence.mp3
Normal file
Binary file not shown.
BIN
public/uisounds/success.mp3
Normal file
BIN
public/uisounds/success.mp3
Normal file
Binary file not shown.
@@ -38,6 +38,11 @@
|
|||||||
Grocy.Api.Post(apiUrl, jsonData,
|
Grocy.Api.Post(apiUrl, jsonData,
|
||||||
function(result)
|
function(result)
|
||||||
{
|
{
|
||||||
|
if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled))
|
||||||
|
{
|
||||||
|
Grocy.UISound.Success();
|
||||||
|
}
|
||||||
|
|
||||||
bookingResponse = result;
|
bookingResponse = result;
|
||||||
|
|
||||||
var addBarcode = GetUriParam('addbarcodetoselection');
|
var addBarcode = GetUriParam('addbarcodetoselection');
|
||||||
@@ -246,6 +251,11 @@ $("#location_id").on('change', function(e)
|
|||||||
|
|
||||||
Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
||||||
{
|
{
|
||||||
|
if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled))
|
||||||
|
{
|
||||||
|
Grocy.UISound.BarcodeScannerBeep();
|
||||||
|
}
|
||||||
|
|
||||||
$("#specific_stock_entry").find("option").remove().end().append("<option></option>");
|
$("#specific_stock_entry").find("option").remove().end().append("<option></option>");
|
||||||
if ($("#use_specific_stock_entry").is(":checked"))
|
if ($("#use_specific_stock_entry").is(":checked"))
|
||||||
{
|
{
|
||||||
@@ -265,13 +275,14 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
|||||||
$('#amount_qu_unit').text(productDetails.quantity_unit_stock.name);
|
$('#amount_qu_unit').text(productDetails.quantity_unit_stock.name);
|
||||||
|
|
||||||
$("#location_id").find("option").remove().end().append("<option></option>");
|
$("#location_id").find("option").remove().end().append("<option></option>");
|
||||||
Grocy.Api.Get("stock/products/" + productId + '/locations',
|
Grocy.Api.Get("stock/products/" + productId + '/locations',
|
||||||
function(stockLocations)
|
function(stockLocations)
|
||||||
{
|
{
|
||||||
var setDefault = 0;
|
var setDefault = 0;
|
||||||
stockLocations.forEach(stockLocation =>
|
stockLocations.forEach(stockLocation =>
|
||||||
{
|
{
|
||||||
if (productDetails.location.id == stockLocation.location_id) {
|
if (productDetails.location.id == stockLocation.location_id)
|
||||||
|
{
|
||||||
$("#location_id").append($("<option>", {
|
$("#location_id").append($("<option>", {
|
||||||
value: stockLocation.location_id,
|
value: stockLocation.location_id,
|
||||||
text: stockLocation.location_name + " (" + __t("Default location") + ")"
|
text: stockLocation.location_name + " (" + __t("Default location") + ")"
|
||||||
@@ -294,6 +305,21 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
|||||||
$("#location_id").trigger('change');
|
$("#location_id").trigger('change');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled))
|
||||||
|
{
|
||||||
|
$("#amount").val(1);
|
||||||
|
Grocy.FrontendHelpers.ValidateForm("consume-form");
|
||||||
|
if (document.getElementById("consume-form").checkValidity() === true)
|
||||||
|
{
|
||||||
|
$('#save-consume-button').click();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toastr.warning(__t("Scan mode is on but not all required fields could be populated automatically"));
|
||||||
|
Grocy.UISound.Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
function(xhr)
|
function(xhr)
|
||||||
{
|
{
|
||||||
@@ -495,3 +521,18 @@ if (GetUriParam("embedded") !== undefined)
|
|||||||
$("#use_specific_stock_entry").trigger('change');
|
$("#use_specific_stock_entry").trigger('change');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default input field
|
||||||
|
Grocy.Components.ProductPicker.GetInputElement().focus();
|
||||||
|
|
||||||
|
// Can only be set via JS however...
|
||||||
|
$("#scan-mode").addClass("user-setting-control");
|
||||||
|
$("#scan-mode").attr("data-setting-key", "scan_mode_consume_enabled");
|
||||||
|
|
||||||
|
$(document).on("change", "#scan-mode", function(e)
|
||||||
|
{
|
||||||
|
if ($(this).prop("checked"))
|
||||||
|
{
|
||||||
|
Grocy.UISound.AskForPermission();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@@ -42,6 +42,11 @@
|
|||||||
Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/add', jsonData,
|
Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/add', jsonData,
|
||||||
function(result)
|
function(result)
|
||||||
{
|
{
|
||||||
|
if (BoolVal(Grocy.UserSettings.scan_mode_purchase_enabled))
|
||||||
|
{
|
||||||
|
Grocy.UISound.Success();
|
||||||
|
}
|
||||||
|
|
||||||
var addBarcode = GetUriParam('addbarcodetoselection');
|
var addBarcode = GetUriParam('addbarcodetoselection');
|
||||||
if (addBarcode !== undefined)
|
if (addBarcode !== undefined)
|
||||||
{
|
{
|
||||||
@@ -122,6 +127,11 @@ if (Grocy.Components.ProductPicker !== undefined)
|
|||||||
{
|
{
|
||||||
Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
||||||
{
|
{
|
||||||
|
if (BoolVal(Grocy.UserSettings.scan_mode_purchase_enabled))
|
||||||
|
{
|
||||||
|
Grocy.UISound.BarcodeScannerBeep();
|
||||||
|
}
|
||||||
|
|
||||||
var productId = $(e.target).val();
|
var productId = $(e.target).val();
|
||||||
|
|
||||||
if (productId)
|
if (productId)
|
||||||
@@ -129,7 +139,7 @@ if (Grocy.Components.ProductPicker !== undefined)
|
|||||||
Grocy.Components.ProductCard.Refresh(productId);
|
Grocy.Components.ProductCard.Refresh(productId);
|
||||||
|
|
||||||
Grocy.Api.Get('stock/products/' + productId,
|
Grocy.Api.Get('stock/products/' + productId,
|
||||||
function (productDetails)
|
function(productDetails)
|
||||||
{
|
{
|
||||||
$('#price').val(productDetails.last_price);
|
$('#price').val(productDetails.last_price);
|
||||||
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
|
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
|
||||||
@@ -205,6 +215,21 @@ if (Grocy.Components.ProductPicker !== undefined)
|
|||||||
$('#amount').focus();
|
$('#amount').focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (BoolVal(Grocy.UserSettings.scan_mode_purchase_enabled))
|
||||||
|
{
|
||||||
|
$("#amount").val(1);
|
||||||
|
Grocy.FrontendHelpers.ValidateForm("purchase-form");
|
||||||
|
if (document.getElementById("purchase-form").checkValidity() === true)
|
||||||
|
{
|
||||||
|
$('#save-purchase-button').click();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toastr.warning(__t("Scan mode is on but not all required fields could be populated automatically"));
|
||||||
|
Grocy.UISound.Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
function(xhr)
|
function(xhr)
|
||||||
{
|
{
|
||||||
@@ -336,3 +361,15 @@ function UndoStockTransaction(transactionId)
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Can only be set via JS however...
|
||||||
|
$("#scan-mode").addClass("user-setting-control");
|
||||||
|
$("#scan-mode").attr("data-setting-key", "scan_mode_purchase_enabled");
|
||||||
|
|
||||||
|
$(document).on("change", "#scan-mode", function(e)
|
||||||
|
{
|
||||||
|
if ($(this).prop("checked"))
|
||||||
|
{
|
||||||
|
Grocy.UISound.AskForPermission();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@@ -440,3 +440,6 @@ if (GetUriParam("embedded") !== undefined)
|
|||||||
$("#use_specific_stock_entry").trigger('change');
|
$("#use_specific_stock_entry").trigger('change');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default input field
|
||||||
|
Grocy.Components.ProductPicker.GetInputElement().focus();
|
||||||
|
@@ -4,10 +4,22 @@
|
|||||||
@section('activeNav', 'consume')
|
@section('activeNav', 'consume')
|
||||||
@section('viewJsName', 'consume')
|
@section('viewJsName', 'consume')
|
||||||
|
|
||||||
|
@push('pageStyles')
|
||||||
|
<link href="{{ $U('/node_modules/bootstrap-switch-button/css/bootstrap-switch-button.css?v=', true) }}{{ $version }}" rel="stylesheet">
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@push('pageScripts')
|
||||||
|
<script src="{{ $U('/node_modules/bootstrap-switch-button/js/bootstrap-switch-button.js?v=', true) }}{{ $version }}"></script>
|
||||||
|
<script src="{{ $U('/js/grocy_uisound.js?v=', true) }}{{ $version }}"></script>
|
||||||
|
@endpush
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-6 col-xl-4 pb-3">
|
<div class="col-xs-12 col-md-6 col-xl-4 pb-3">
|
||||||
<h1>@yield('title')</h1>
|
<h1>
|
||||||
|
@yield('title')
|
||||||
|
<input @if(boolval($userSettings['scan_mode_consume_enabled'])) checked @endif id="scan-mode" type="checkbox" data-setting-key="scan_mode_consume_enabled" data-toggle="switchbutton" data-onlabel="{{ $__t('Scan mode') }} {{ $__t('on') }}" data-offlabel="{{ $__t('Scan mode') }} {{ $__t('off') }}" data-onstyle="success" data-offstyle="primary" data-style="ml-2" data-width="160">
|
||||||
|
</h1>
|
||||||
|
|
||||||
<form id="consume-form" novalidate>
|
<form id="consume-form" novalidate>
|
||||||
|
|
||||||
@@ -28,23 +40,23 @@
|
|||||||
))
|
))
|
||||||
|
|
||||||
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
|
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
|
||||||
@php /*@include('components.locationpicker', array(
|
@php /*@include('components.locationpicker', array(
|
||||||
'id' => 'location_id',
|
'id' => 'location_id',
|
||||||
'locations' => $locations,
|
'locations' => $locations,
|
||||||
'isRequired' => true,
|
'isRequired' => true,
|
||||||
'label' => 'Location'
|
'label' => 'Location'
|
||||||
))*/ @endphp
|
))*/ @endphp
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="location_id">{{ $__t('Location') }}</label>
|
<label for="location_id">{{ $__t('Location') }}</label>
|
||||||
<select required class="form-control location-combobox" id="location_id" name="location_id">
|
<select required class="form-control location-combobox" id="location_id" name="location_id">
|
||||||
<option></option>
|
<option></option>
|
||||||
@foreach($locations as $location)
|
@foreach($locations as $location)
|
||||||
<option value="{{ $location->id }}">{{ $location->name }}</option>
|
<option value="{{ $location->id }}">{{ $location->name }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
<div class="invalid-feedback">{{ $__t('A location is required') }}</div>
|
<div class="invalid-feedback">{{ $__t('A location is required') }}</div>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<input type="hidden" name="location_id" id="location_id" value="1">
|
<input type="hidden" name="location_id" id="location_id" value="1">
|
||||||
@endif
|
@endif
|
||||||
|
@@ -4,10 +4,22 @@
|
|||||||
@section('activeNav', 'purchase')
|
@section('activeNav', 'purchase')
|
||||||
@section('viewJsName', 'purchase')
|
@section('viewJsName', 'purchase')
|
||||||
|
|
||||||
|
@push('pageStyles')
|
||||||
|
<link href="{{ $U('/node_modules/bootstrap-switch-button/css/bootstrap-switch-button.css?v=', true) }}{{ $version }}" rel="stylesheet">
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@push('pageScripts')
|
||||||
|
<script src="{{ $U('/node_modules/bootstrap-switch-button/js/bootstrap-switch-button.js?v=', true) }}{{ $version }}"></script>
|
||||||
|
<script src="{{ $U('/js/grocy_uisound.js?v=', true) }}{{ $version }}"></script>
|
||||||
|
@endpush
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-6 col-xl-4 pb-3">
|
<div class="col-xs-12 col-md-6 col-xl-4 pb-3">
|
||||||
<h1>@yield('title')</h1>
|
<h1>
|
||||||
|
@yield('title')
|
||||||
|
<input @if(boolval($userSettings['scan_mode_purchase_enabled'])) checked @endif id="scan-mode" type="checkbox" data-setting-key="scan_mode_purchase_enabled" data-toggle="switchbutton" data-onlabel="{{ $__t('Scan mode') }} {{ $__t('on') }}" data-offlabel="{{ $__t('Scan mode') }} {{ $__t('off') }}" data-onstyle="success" data-offstyle="primary" data-style="ml-2" data-width="160">
|
||||||
|
</h1>
|
||||||
|
|
||||||
<form id="purchase-form" novalidate>
|
<form id="purchase-form" novalidate>
|
||||||
|
|
||||||
|
14
yarn.lock
14
yarn.lock
@@ -17,6 +17,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
jquery "1"
|
jquery "1"
|
||||||
|
|
||||||
|
add@^2.0.6:
|
||||||
|
version "2.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/add/-/add-2.0.6.tgz#248f0a9f6e5a528ef2295dbeec30532130ae2235"
|
||||||
|
integrity sha1-JI8Kn25aUo7yKV2+7DBTITCuIjU=
|
||||||
|
|
||||||
ajv@^6.5.5:
|
ajv@^6.5.5:
|
||||||
version "6.10.2"
|
version "6.10.2"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52"
|
||||||
@@ -75,6 +80,10 @@ bootstrap-select@^1.13.10:
|
|||||||
resolved "https://registry.yarnpkg.com/bootstrap-select/-/bootstrap-select-1.13.11.tgz#d364ccc67cd6c1f9158689087f1d2d029df82c29"
|
resolved "https://registry.yarnpkg.com/bootstrap-select/-/bootstrap-select-1.13.11.tgz#d364ccc67cd6c1f9158689087f1d2d029df82c29"
|
||||||
integrity sha512-WPpx2DYL9jVilNoqy4Pjcfa/Q0LOq8V0+xws/pmnRDn/deS7OYjo1njvD1Cv0s9/1ZUXt77UypxuloimEnkYsA==
|
integrity sha512-WPpx2DYL9jVilNoqy4Pjcfa/Q0LOq8V0+xws/pmnRDn/deS7OYjo1njvD1Cv0s9/1ZUXt77UypxuloimEnkYsA==
|
||||||
|
|
||||||
|
"bootstrap-switch-button@https://github.com/walidbagh/bootstrap-switch-button#Fix-module-export":
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://github.com/walidbagh/bootstrap-switch-button#551ccd361cc9e291cd04c2278357b1db76318f1e"
|
||||||
|
|
||||||
bootstrap@4.0.0:
|
bootstrap@4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.0.0.tgz#ceb03842c145fcc1b9b4e15da2a05656ba68469a"
|
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.0.0.tgz#ceb03842c145fcc1b9b4e15da2a05656ba68469a"
|
||||||
@@ -779,3 +788,8 @@ verror@1.10.0:
|
|||||||
assert-plus "^1.0.0"
|
assert-plus "^1.0.0"
|
||||||
core-util-is "1.0.2"
|
core-util-is "1.0.2"
|
||||||
extsprintf "^1.2.0"
|
extsprintf "^1.2.0"
|
||||||
|
|
||||||
|
yarn@^1.21.1:
|
||||||
|
version "1.21.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.21.1.tgz#1d5da01a9a03492dc4a5957befc1fd12da83d89c"
|
||||||
|
integrity sha512-dQgmJv676X/NQczpbiDtc2hsE/pppGDJAzwlRiADMTvFzYbdxPj2WO4PcNyriSt2c4jsCMpt8UFRKHUozt21GQ==
|
||||||
|
Reference in New Issue
Block a user