mirror of
https://github.com/acemod/ACE3.git
synced 2024-08-30 18:23:18 +00:00
Arsenal - Improved sorting, insignia detection, identity info verification and minor cleanup (#9795)
* Changed sorting + minor cleanup * Update addons/arsenal/functions/fnc_fillLeftPanel.sqf Co-authored-by: Grim <69561145+LinkIsGrim@users.noreply.github.com> * check and log missing extended info * fix extended loadout logging * Made voice stuff config case * use lowercase voice everywhere * check loadout voice against configCase * Update fnc_verifyLoadout.sqf * Update addons/arsenal/functions/fnc_verifyLoadout.sqf Co-authored-by: Grim <69561145+LinkIsGrim@users.noreply.github.com> * Update addons/arsenal/functions/fnc_verifyLoadout.sqf Co-authored-by: Grim <69561145+LinkIsGrim@users.noreply.github.com> * Update addons/arsenal/functions/fnc_verifyLoadout.sqf --------- Co-authored-by: LinkIsGrim <salluci.lovi@gmail.com> Co-authored-by: Grim <69561145+LinkIsGrim@users.noreply.github.com>
This commit is contained in:
parent
a0f3933bf0
commit
fd5e56ffbc
@ -108,24 +108,16 @@ GVAR(lastSortDirectionRight) = DESCENDING;
|
||||
private _face = _extendedInfo getOrDefault [QGVAR(face), ""];
|
||||
|
||||
if (_face != "") then {
|
||||
if (isMultiplayer) then {
|
||||
private _id = [QGVAR(broadcastFace), [_unit, _face], QGVAR(centerFace_) + netId _unit] call CBA_fnc_globalEventJIP;
|
||||
[_id, _unit] call CBA_fnc_removeGlobalEventJIP;
|
||||
} else {
|
||||
_unit setFace _face;
|
||||
};
|
||||
private _id = [QGVAR(broadcastFace), [_unit, _face], QGVAR(centerFace_) + hashValue _unit] call CBA_fnc_globalEventJIP;
|
||||
[_id, _unit] call CBA_fnc_removeGlobalEventJIP;
|
||||
};
|
||||
|
||||
// Set voice
|
||||
private _voice = _extendedInfo getOrDefault [QGVAR(voice), ""];
|
||||
|
||||
if (_voice != "") then {
|
||||
if (isMultiplayer) then {
|
||||
private _id = [QGVAR(broadcastVoice), [_unit, _voice], QGVAR(centerVoice_) + netId _unit] call CBA_fnc_globalEventJIP;
|
||||
[_id, _unit] call CBA_fnc_removeGlobalEventJIP;
|
||||
} else {
|
||||
_unit setSpeaker _voice;
|
||||
};
|
||||
private _id = [QGVAR(broadcastVoice), [_unit, _voice], QGVAR(centerVoice_) + hashValue _unit] call CBA_fnc_globalEventJIP;
|
||||
[_id, _unit] call CBA_fnc_removeGlobalEventJIP;
|
||||
};
|
||||
|
||||
// Set insignia
|
||||
@ -147,7 +139,7 @@ GVAR(lastSortDirectionRight) = DESCENDING;
|
||||
|
||||
// Set voice if enabled
|
||||
if (GVAR(loadoutsSaveVoice)) then {
|
||||
_extendedInfo set [QGVAR(voice), speaker _unit];
|
||||
_extendedInfo set [QGVAR(voice), (speaker _unit) call EFUNC(common,getConfigName)];
|
||||
};
|
||||
|
||||
// Set insignia if enabled
|
||||
|
@ -91,5 +91,4 @@ private _insigniaCondition = toString {
|
||||
GVAR(insigniaCache) set [_x, 2];
|
||||
} forEach (_insigniaCondition configClasses (missionConfigFile >> "CfgUnitInsignia"));
|
||||
|
||||
|
||||
ADDON = true;
|
||||
|
@ -6,7 +6,7 @@
|
||||
*
|
||||
* Arguments:
|
||||
* 0: Config category, must be "CfgWeapons", "CfgVehicles", "CfgMagazines", "CfgVoice" or "CfgUnitInsignia" <STRING>
|
||||
* 1: Classname <STRING>
|
||||
* 1: Classname (must be in config case) <STRING>
|
||||
* 2: Panel control <CONTROL>
|
||||
* 3: Name of the picture entry in that Cfg class <STRING> (default: "picture")
|
||||
* 4: Config root <NUMBER> (default: 0 -> configFile)
|
||||
@ -42,7 +42,7 @@ if (_skip) then {
|
||||
|
||||
if (_skip) exitWith {};
|
||||
|
||||
// Sanitise key, as it's public; If not in cache, find info and cache it for later use
|
||||
// If not in cache, find info and cache it for later use
|
||||
((uiNamespace getVariable QGVAR(addListBoxItemCache)) getOrDefaultCall [_configCategory + _className + str _configRoot, {
|
||||
// Get classname (config case), display name, picture and DLC
|
||||
private _configPath = ([configFile, campaignConfigFile, missionConfigFile] select _configRoot) >> _configCategory >> _className;
|
||||
|
@ -71,15 +71,16 @@ if (GVAR(currentLoadoutsTab) != IDC_buttonSharedLoadouts) then {
|
||||
_loadoutCachedInfo = [_loadoutData] call FUNC(verifyLoadout);
|
||||
_contentPanelCtrl setVariable [_loadoutNameAndTab, _loadoutCachedInfo];
|
||||
|
||||
_loadoutCachedInfo params ["", "_nullItemsList", "_unavailableItemsList"];
|
||||
_loadoutCachedInfo params ["", "_nullItemsList", "_unavailableItemsList", "_missingExtendedInfo"];
|
||||
|
||||
// Log missing / nil items to RPT (only once per arsenal session)
|
||||
if (GVAR(EnableRPTLog) && {(_nullItemsList isNotEqualTo []) || {_unavailableItemsList isNotEqualTo []}}) then {
|
||||
if (GVAR(EnableRPTLog) && {(_nullItemsList isNotEqualTo []) || {_unavailableItemsList isNotEqualTo [] || {_missingExtendedInfo isNotEqualTo []}}}) then {
|
||||
private _printComponent = "ACE_Arsenal - Loadout:";
|
||||
private _printNullItemsList = ["Missing items:", str _nullItemsList] joinString " ";
|
||||
private _printUnavailableItemsList = ["Unavailable items:", str _unavailableItemsList] joinString " ";
|
||||
private _printMissingExtendedInfo = ["Missing extended loadout:", str _missingExtendedInfo] joinString " ";
|
||||
|
||||
diag_log text (format ["%1%5 %2%5 %3%5 %4", _printComponent, "Name: " + _loadoutName, _printNullItemsList, _printUnavailableItemsList, endl]);
|
||||
diag_log text (format ["%1%6 %2%6 %3%6 %4%6 %5", _printComponent, "Name: " + _loadoutName, _printNullItemsList, _printUnavailableItemsList, _printMissingExtendedInfo, endl]);
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -68,7 +68,7 @@ if (isNil QGVAR(virtualItems)) then {
|
||||
GVAR(virtualItemsFlatAll) = +GVAR(virtualItemsFlat);
|
||||
|
||||
GVAR(currentFace) = face GVAR(center);
|
||||
GVAR(currentVoice) = speaker GVAR(center);
|
||||
GVAR(currentVoice) = (speaker GVAR(center)) call EFUNC(common,getConfigName);
|
||||
GVAR(currentInsignia) = GVAR(center) call BIS_fnc_getUnitInsignia;
|
||||
|
||||
GVAR(currentAction) = "Stand";
|
||||
|
@ -683,14 +683,19 @@ switch (GVAR(currentLeftPanel)) do {
|
||||
|
||||
TOGGLE_RIGHT_PANEL_HIDE
|
||||
|
||||
private _unitInsigniaConfig = configFile >> "CfgUnitInsignia" >> _item;
|
||||
// Check for correct config: First mission, then campaign and finally regular config
|
||||
private _itemCfg = missionConfigFile >> "CfgUnitInsignia" >> _item;
|
||||
|
||||
if (isNull _itemCfg) then {
|
||||
_itemCfg = campaignConfigFile >> "CfgUnitInsignia" >> _item;
|
||||
};
|
||||
|
||||
if (isNull _itemCfg) then {
|
||||
_itemCfg = configFile >> "CfgUnitInsignia" >> _item;
|
||||
};
|
||||
|
||||
// Display new items's info on the bottom right
|
||||
if (isNull _unitInsigniaConfig) then {
|
||||
[_display, _control, _curSel, missionConfigFile >> "CfgUnitInsignia" >> _item] call FUNC(itemInfo);
|
||||
} else {
|
||||
[_display, _control, _curSel, _unitInsigniaConfig] call FUNC(itemInfo);
|
||||
};
|
||||
[_display, _control, _curSel, _itemCfg] call FUNC(itemInfo);
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -15,6 +15,10 @@
|
||||
|
||||
params ["_control"];
|
||||
|
||||
// https://community.bistudio.com/wiki/toString, see comment
|
||||
// However, using 55295 did not work as expected, 55291 was found by trial and error
|
||||
#define HIGHEST_VALUE_CHAR 55291
|
||||
|
||||
// When filling the sorting panel, FUNC(sortPanel) is called twice, so ignore first call
|
||||
if (GVAR(ignoreFirstSortPanelCall)) exitWith {
|
||||
GVAR(ignoreFirstSortPanelCall) = false;
|
||||
@ -29,6 +33,7 @@ private _sortDirectionCtrl = _display displayCtrl ([IDC_sortLeftTabDirection, ID
|
||||
private _cfgMagazines = configFile >> "CfgMagazines";
|
||||
private _cfgFaces = configFile >> "CfgFaces";
|
||||
private _cfgUnitInsignia = configFile >> "CfgUnitInsignia";
|
||||
private _cfgUnitInsigniaCampaign = campaignConfigFile >> "CfgUnitInsignia";
|
||||
private _cfgUnitInsigniaMission = missionConfigFile >> "CfgUnitInsignia";
|
||||
|
||||
if (_rightSort) then {
|
||||
@ -126,7 +131,6 @@ private _selected = if (_right) then {
|
||||
_panel lbData _curSel
|
||||
};
|
||||
|
||||
private _originalNames = createHashMap;
|
||||
private _item = "";
|
||||
private _quantity = "";
|
||||
private _itemCfg = configNull;
|
||||
@ -136,12 +140,8 @@ private _fillerChar = toString [1];
|
||||
|
||||
private _magazineMiscItems = uiNamespace getVariable QGVAR(magazineMiscItems);
|
||||
private _sortCache = uiNamespace getVariable QGVAR(sortCache);
|
||||
|
||||
private _faceCache = if (_cfgClass == _cfgFaces) then {
|
||||
uiNamespace getVariable [QGVAR(faceCache), createHashMap]
|
||||
} else {
|
||||
createHashMap
|
||||
};
|
||||
private _faceCache = uiNamespace getVariable QGVAR(faceCache);
|
||||
private _insigniaCache = uiNamespace getVariable QGVAR(insigniaCache);
|
||||
|
||||
private _countColumns = if (_right) then {
|
||||
count lnbGetColumnsPosition _panel
|
||||
@ -150,9 +150,9 @@ private _countColumns = if (_right) then {
|
||||
};
|
||||
|
||||
private _for = if (_right) then {
|
||||
for '_i' from 0 to (lnbSize _panel select 0) - 1
|
||||
for "_i" from 0 to (lnbSize _panel select 0) - 1
|
||||
} else {
|
||||
for '_i' from 0 to (lbSize _panel) - 1
|
||||
for "_i" from 0 to (lbSize _panel) - 1
|
||||
};
|
||||
|
||||
_for do {
|
||||
@ -163,6 +163,14 @@ _for do {
|
||||
_panel lbData _i
|
||||
};
|
||||
|
||||
// Check if entry is "Empty"
|
||||
if (!_right && {(_panel lbValue _i) == -1}) then {
|
||||
// Set to lowest/highest lexicographical value, so that "Empty" is always at the top
|
||||
_panel lbSetTextRight [_i, ["", toString [HIGHEST_VALUE_CHAR, HIGHEST_VALUE_CHAR, HIGHEST_VALUE_CHAR, HIGHEST_VALUE_CHAR, HIGHEST_VALUE_CHAR]] select (_sortDirection == ASCENDING)];
|
||||
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get item's count
|
||||
_quantity = if (_right) then {
|
||||
parseNumber (_panel lnbText [_i, 2])
|
||||
@ -179,18 +187,22 @@ _for do {
|
||||
_itemCfg = if !(_cfgClass in [_cfgFaces, _cfgUnitInsignia]) then {
|
||||
_cfgClass >> _item
|
||||
} else {
|
||||
// If insignia, check both config and mission file
|
||||
// If insignia, check for correct config: First mission, then campaign and finally regular config
|
||||
if (_cfgClass == _cfgUnitInsignia) then {
|
||||
_itemCfg = _cfgClass >> _item;
|
||||
_itemCfg = _cfgUnitInsigniaMission >> _item;
|
||||
|
||||
if (isNull _itemCfg) then {
|
||||
_itemCfg = _cfgUnitInsigniaMission >> _item;
|
||||
_itemCfg = _cfgUnitInsigniaCampaign >> _item;
|
||||
};
|
||||
|
||||
if (isNull _itemCfg) then {
|
||||
_itemCfg = _cfgUnitInsignia >> _item;
|
||||
};
|
||||
|
||||
_itemCfg
|
||||
} else {
|
||||
// If face, check correct category
|
||||
_cfgClass >> (_faceCache get _item) param [2, "Man_A3"] >> _item
|
||||
_cfgClass >> (_faceCache getOrDefault [_item, []]) param [2, "Man_A3"] >> _item
|
||||
};
|
||||
};
|
||||
|
||||
@ -216,37 +228,29 @@ _for do {
|
||||
_value
|
||||
}, true];
|
||||
|
||||
// Save the current row's item's name in a cache and set text to it's sorting value
|
||||
// Set the right text temporarily, so it can be used for sorting
|
||||
if (_right) then {
|
||||
_name = _panel lnbText [_i, 1];
|
||||
_originalNames set [_item, _name];
|
||||
|
||||
// Use value, display name and classname to sort, which means a fixed alphabetical order is guaranteed
|
||||
// Filler char has lowest lexicographical order possible
|
||||
_panel lnbSetText [[_i, 1], format ["%1%2%4%3", _value, _name, _item, _fillerChar]];
|
||||
// Filler char has lowest lexicographical value possible
|
||||
_panel lnbSetTextRight [[_i, 1], format ["%1%2%4%3", _value, _panel lnbText [_i, 1], _item, _fillerChar]];
|
||||
} else {
|
||||
if (_item != "") then {
|
||||
_name = _panel lbText _i;
|
||||
_originalNames set [_item, _name];
|
||||
|
||||
// Use value, display name and classname to sort, which means a fixed alphabetical order is guaranteed
|
||||
// Filler char has lowest lexicographical order possible
|
||||
_panel lbSetText [_i, format ["%1%2%4%3", _value, _name, _item, _fillerChar]];
|
||||
// Filler char has lowest lexicographical value possible
|
||||
_panel lbSetTextRight [_i, format ["%1%2%4%3", _value, _panel lbText _i, _item, _fillerChar]];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// Sort alphabetically, find the previously selected item, select it again and reset text to original text
|
||||
// Sort alphabetically, find the previously selected item and select it again
|
||||
if (_right) then {
|
||||
_panel lnbSort [1, _sortDirection == ASCENDING];
|
||||
[_panel, 1] lnbSortBy ["TEXT", _sortDirection == ASCENDING, false, true, false];
|
||||
|
||||
_for do {
|
||||
_item = _panel lnbData [_i, 0];
|
||||
// Remove sorting text, as it blocks the item name otherwise
|
||||
_panel lnbSetTextRight [[_i, 1], ""];
|
||||
|
||||
_panel lnbSetText [[_i, 1], _originalNames get _item];
|
||||
|
||||
// Set selection after text, otherwise item info box on the right side shows invalid name
|
||||
if (_curSel != -1 && {_item == _selected}) then {
|
||||
if (_curSel != -1 && {(_panel lnbData [_i, 0]) == _selected}) then {
|
||||
_panel lnbSetCurSelRow _i;
|
||||
|
||||
// To avoid unnecessary checks after previsouly selected item was found
|
||||
@ -254,17 +258,17 @@ if (_right) then {
|
||||
};
|
||||
};
|
||||
} else {
|
||||
lbSort [_panel, ["DESC", "ASC"] select _sortDirection];
|
||||
_panel lbSortBy ["TEXT", _sortDirection == ASCENDING, false, true, false];
|
||||
|
||||
_for do {
|
||||
_item = _panel lbData _i;
|
||||
|
||||
// Check if valid item (problems can be caused when searching)
|
||||
if (_item != "") then {
|
||||
_panel lbSetText [_i, _originalNames get _item];
|
||||
// Remove sorting text, as it blocks the item name otherwise
|
||||
_panel lbSetTextRight [_i, ""];
|
||||
};
|
||||
|
||||
// Set selection after text, otherwise item info box on the right side shows invalid name
|
||||
if (_curSel != -1 && {_item == _selected}) then {
|
||||
_panel lbSetCurSel _i;
|
||||
|
||||
|
@ -27,6 +27,7 @@ private _name = "";
|
||||
private _itemArray = [];
|
||||
private _nullItemsList = [];
|
||||
private _unavailableItemsList = [];
|
||||
private _missingExtendedInfo = [];
|
||||
|
||||
// Search for all items and check their availability
|
||||
private _fnc_filterLoadout = {
|
||||
@ -70,7 +71,22 @@ private _fnc_filterLoadout = {
|
||||
// Loadout might come from a different modpack, which might have different config naming
|
||||
_loadout = _loadout call _fnc_filterLoadout;
|
||||
|
||||
// Raise event for 3rd party: mostly for handling extended info
|
||||
[QGVAR(loadoutVerified), [_loadout, _extendedInfo]] call CBA_fnc_localEvent;
|
||||
{
|
||||
private _class = _extendedInfo getOrDefault [_x, ""];
|
||||
private _cache = missionNamespace getVariable (_x + "Cache");
|
||||
|
||||
[[_loadout, _extendedInfo], _nullItemsList arrayIntersect _nullItemsList, _unavailableItemsList arrayIntersect _unavailableItemsList]
|
||||
// Previously voices were stored in lower case (speaker command returns lower case), so this is to make old loadouts compatible
|
||||
if (_class != "" && {_x == QGVAR(voice)}) then {
|
||||
_class = _class call EFUNC(common,getConfigName);
|
||||
};
|
||||
if (_class != "" && {!(_class in _cache)}) then {
|
||||
_missingExtendedInfo pushBack [_x, _class];
|
||||
_extendedInfo deleteAt _x;
|
||||
};
|
||||
} forEach [QGVAR(insignia), QGVAR(face), QGVAR(voice)];
|
||||
|
||||
// Raise event for 3rd party: mostly for handling extended info
|
||||
// Pass all items, including duplicates
|
||||
[QGVAR(loadoutVerified), [_loadout, _extendedInfo, _nullItemsList, _unavailableItemsList, _missingExtendedInfo]] call CBA_fnc_localEvent;
|
||||
|
||||
[[_loadout, _extendedInfo], _nullItemsList arrayIntersect _nullItemsList, _unavailableItemsList arrayIntersect _unavailableItemsList, _missingExtendedInfo]
|
||||
|
@ -564,6 +564,7 @@ class GVAR(display) {
|
||||
colorSelect[] = {1,1,1,1};
|
||||
colorSelect2[] = {1,1,1,1};
|
||||
colorPictureRightSelected[] = {1,1,1,1};
|
||||
colorTextRight[] = {0.5, 0.5, 0.5, 0};
|
||||
onLBSelChanged = QUOTE(_this call FUNC(onSelChangedLeft));
|
||||
onLBDblClick = QUOTE(_this call FUNC(onPanelDblClick));
|
||||
onSetFocus = QUOTE(GVAR(leftTabFocus) = true);
|
||||
@ -594,6 +595,7 @@ class GVAR(display) {
|
||||
colorSelect[] = {1,1,1,1};
|
||||
colorSelect2[] = {1,1,1,1};
|
||||
colorPictureRightSelected[] = {1,1,1,1};
|
||||
colorTextRight[] = {0.5, 0.5, 0.5, 0};
|
||||
columns[] = {0.07, 0.15, 0.75};
|
||||
idcLeft = IDC_arrowMinus;
|
||||
idcRIght = IDC_arrowPlus;
|
||||
|
@ -1 +0,0 @@
|
||||
#include "..\script_component.hpp"
|
@ -17,7 +17,7 @@
|
||||
|
||||
params ["_className"];
|
||||
|
||||
(uiNamespace getVariable QGVAR(configNames)) getOrDefaultCall [toLower _className, {
|
||||
(uiNamespace getVariable QGVAR(configNames)) getOrDefaultCall [toLowerANSI _className, {
|
||||
private _config = configNull;
|
||||
|
||||
{
|
||||
|
@ -27,23 +27,34 @@ PREP_RECOMPILE_END;
|
||||
}] call CBA_fnc_addClassEventHandler;
|
||||
|
||||
[QEGVAR(arsenal,loadoutVerified), {
|
||||
params ["_loadout", "_extendedInfo"];
|
||||
params ["_loadout", "_extendedInfo", "", "", "_missingExtendedInfo"];
|
||||
private _gunbagInfo = _extendedInfo getOrDefault [QGVAR(gunbagWeapon), []];
|
||||
if (_gunbagInfo isEqualTo []) exitWith {};
|
||||
|
||||
private _weapon = (_gunbagInfo select 0) call EFUNC(arsenal,baseWeapon);
|
||||
if !(_weapon in EGVAR(arsenal,virtualItemsFlat)) exitWith {
|
||||
INFO_1("removing [%1] from loadout",_gunbagInfo);
|
||||
_missingExtendedInfo pushBack [QGVAR(gunbagWeapon), _weapon];
|
||||
_extendedInfo deleteAt QGVAR(gunbagWeapon);
|
||||
};
|
||||
private _missingItems = [];
|
||||
private _attachments = _gunbagInfo select 1;
|
||||
{
|
||||
if (_x != "" && {!(_x call EFUNC(arsenal,baseWeapon) in EGVAR(arsenal,virtualItemsFlat))}) then {
|
||||
_missingItems pushBack _x;
|
||||
_attachments set [_forEachIndex, ""];
|
||||
};
|
||||
} forEach _attachments;
|
||||
private _magazines = _gunbagInfo select 2;
|
||||
{
|
||||
private _class = _x param [0, ""];
|
||||
private _defaultValue = ["", []] select {_x isEqualType []};
|
||||
if (_class != "" && {!(_class in EGVAR(arsenal,virtualItemsFlat))}) then {
|
||||
INFO_1("removing [%1] from loadout",_x);
|
||||
_gunbagInfo set [_forEachIndex + 1, _defaultValue];
|
||||
if !(_class != "" && {_class in EGVAR(arsenal,virtualItemsFlat)}) then {
|
||||
_missingItems pushBack _class;
|
||||
_magazines set [_forEachIndex, ["", 0]];
|
||||
};
|
||||
} forEach (_gunbagInfo select [1]); // weapon was verified above
|
||||
} forEach _magazines;
|
||||
if (_missingItems isNotEqualTo []) then {
|
||||
_missingExtendedInfo pushBack [QGVAR(gunbagWeapon), _missingItems];
|
||||
};
|
||||
}] call CBA_fnc_addEventHandler;
|
||||
|
||||
["CBA_loadoutSet", {
|
||||
|
@ -511,7 +511,7 @@ All are local.
|
||||
| ace_arsenal_loadoutsDisplayClosed | None | 3.12.3 |
|
||||
| ace_arsenal_loadoutsTabChanged | loadouts screen display (DISPLAY), tab control (CONTROL) | 3.12.3 |
|
||||
| ace_arsenal_loadoutsListFilled | loadouts screen display (DISPLAY), tab control (CONTROL) | 3.12.3 |
|
||||
| ace_arsenal_loadoutVerified | loadout data (ARRAY), loadout CBA extended data (HASHMAP) | 3.17.0 |
|
||||
| ace_arsenal_loadoutVerified | loadout data (ARRAY), loadout CBA extended data (HASHMAP), null items (ARRAY), unavailable items (ARRAY), unavailable extended data (ARRAY) | 3.17.0 |
|
||||
| ace_arsenal_weaponItemChanged | weapon classname (STRING), item classname (STRING), item index (NUMBER, 0-5: muzzle, side, optic, bipod, magazine, underbarrel) | 3.16.0 |
|
||||
|
||||
## 9. Custom sub item categories
|
||||
|
Loading…
Reference in New Issue
Block a user