| | 1 | | using Cysharp.Threading.Tasks; |
| | 2 | | using DCL.Browser; |
| | 3 | | using DCL.Helpers; |
| | 4 | | using DCL.Tasks; |
| | 5 | | using DCLServices.CustomNftCollection; |
| | 6 | | using DCLServices.WearablesCatalogService; |
| | 7 | | using MainScripts.DCL.Controllers.HUD.CharacterPreview; |
| | 8 | | using System; |
| | 9 | | using System.Collections.Generic; |
| | 10 | | using System.Linq; |
| | 11 | | using System.Threading; |
| | 12 | | using System.Threading.Tasks; |
| | 13 | | using UnityEngine; |
| | 14 | | using UnityEngine.Pool; |
| | 15 | |
|
| | 16 | | namespace DCL.Backpack |
| | 17 | | { |
| | 18 | | public class WearableGridController |
| | 19 | | { |
| | 20 | | private const int PAGE_SIZE = 15; |
| | 21 | | private const string ALL_FILTER_REF = "all"; |
| | 22 | | private const string NAME_FILTER_REF = "name="; |
| | 23 | | private const string CATEGORY_FILTER_REF = "category="; |
| | 24 | | private const string URL_MARKET_PLACE = "https://market.decentraland.org/browse?section=wearables"; |
| | 25 | | private const string URL_GET_A_WALLET = "https://docs.decentraland.org/get-a-wallet"; |
| | 26 | | private const string EMPTY_WEARABLE_DESCRIPTION = "This item doesn’t have a description."; |
| | 27 | |
|
| | 28 | | private readonly IWearableGridView view; |
| | 29 | | private readonly IUserProfileBridge userProfileBridge; |
| | 30 | | private readonly IWearablesCatalogService wearablesCatalogService; |
| | 31 | | private readonly DataStore_BackpackV2 dataStoreBackpackV2; |
| | 32 | | private readonly IBrowserBridge browserBridge; |
| | 33 | | private readonly BackpackFiltersController backpackFiltersController; |
| | 34 | | private readonly AvatarSlotsHUDController avatarSlotsHUDController; |
| | 35 | | private readonly IBackpackAnalyticsService backpackAnalyticsService; |
| | 36 | | private readonly ICustomNftCollectionService customNftCollectionService; |
| 63 | 37 | | private readonly List<WearableItem> customWearablesBuffer = new (); |
| | 38 | |
|
| 63 | 39 | | private Dictionary<string, WearableGridItemModel> currentWearables = new (); |
| 63 | 40 | | private CancellationTokenSource requestWearablesCancellationToken = new (); |
| 63 | 41 | | private CancellationTokenSource filtersCancellationToken = new (); |
| | 42 | | private string categoryFilter; |
| | 43 | | private ICollection<string> thirdPartyCollectionIdsFilter; |
| | 44 | | private string nameFilter; |
| | 45 | |
|
| | 46 | | // initialize as "newest" |
| 63 | 47 | | private (NftOrderByOperation type, bool directionAscendent)? wearableSorting = new (NftOrderByOperation.Date, fa |
| 63 | 48 | | private NftCollectionType collectionTypeMask = NftCollectionType.Base | NftCollectionType.OnChain; |
| | 49 | |
|
| | 50 | | public event Action<string> OnWearableSelected; |
| | 51 | | public event Action<string, EquipWearableSource> OnWearableEquipped; |
| | 52 | | public event Action<string, UnequipWearableSource> OnWearableUnequipped; |
| | 53 | | public event Action OnCategoryFilterRemoved; |
| | 54 | |
|
| 63 | 55 | | public WearableGridController(IWearableGridView view, |
| | 56 | | IUserProfileBridge userProfileBridge, |
| | 57 | | IWearablesCatalogService wearablesCatalogService, |
| | 58 | | DataStore_BackpackV2 dataStoreBackpackV2, |
| | 59 | | IBrowserBridge browserBridge, |
| | 60 | | BackpackFiltersController backpackFiltersController, |
| | 61 | | AvatarSlotsHUDController avatarSlotsHUDController, |
| | 62 | | IBackpackAnalyticsService backpackAnalyticsService, |
| | 63 | | ICustomNftCollectionService customNftCollectionService) |
| | 64 | | { |
| 63 | 65 | | this.view = view; |
| 63 | 66 | | this.userProfileBridge = userProfileBridge; |
| 63 | 67 | | this.wearablesCatalogService = wearablesCatalogService; |
| 63 | 68 | | this.dataStoreBackpackV2 = dataStoreBackpackV2; |
| 63 | 69 | | this.browserBridge = browserBridge; |
| 63 | 70 | | this.backpackFiltersController = backpackFiltersController; |
| 63 | 71 | | this.avatarSlotsHUDController = avatarSlotsHUDController; |
| 63 | 72 | | this.backpackAnalyticsService = backpackAnalyticsService; |
| 63 | 73 | | this.customNftCollectionService = customNftCollectionService; |
| | 74 | |
|
| 63 | 75 | | view.OnWearablePageChanged += HandleNewPageRequested; |
| 63 | 76 | | view.OnWearableEquipped += HandleWearableEquipped; |
| 63 | 77 | | view.OnWearableUnequipped += HandleWearableUnequipped; |
| 63 | 78 | | view.OnWearableSelected += HandleWearableSelected; |
| 63 | 79 | | view.OnFilterSelected += FilterWearablesFromReferencePath; |
| 63 | 80 | | view.OnFilterRemoved += RemoveFiltersFromReferencePath; |
| 63 | 81 | | view.OnGoToMarketplace += GoToMarketplace; |
| | 82 | |
|
| 63 | 83 | | backpackFiltersController.OnThirdPartyCollectionChanged += SetThirdPartCollectionIds; |
| 63 | 84 | | backpackFiltersController.OnSortByChanged += SetSorting; |
| 63 | 85 | | backpackFiltersController.OnSearchTextChanged += SetNameFilterFromSearchText; |
| 63 | 86 | | backpackFiltersController.OnCollectionTypeChanged += SetCollectionTypeFromFilterSelection; |
| | 87 | |
|
| 63 | 88 | | avatarSlotsHUDController.OnToggleSlot += SetCategoryFromFilterSelection; |
| 63 | 89 | | } |
| | 90 | |
|
| | 91 | | public void Dispose() |
| | 92 | | { |
| 63 | 93 | | view.OnWearablePageChanged -= HandleNewPageRequested; |
| 63 | 94 | | view.OnWearableEquipped -= HandleWearableEquipped; |
| 63 | 95 | | view.OnWearableUnequipped -= HandleWearableUnequipped; |
| 63 | 96 | | view.OnWearableSelected -= HandleWearableSelected; |
| 63 | 97 | | view.OnFilterRemoved -= RemoveFiltersFromReferencePath; |
| 63 | 98 | | view.OnFilterSelected -= FilterWearablesFromReferencePath; |
| 63 | 99 | | view.OnGoToMarketplace -= GoToMarketplace; |
| | 100 | |
|
| 63 | 101 | | backpackFiltersController.OnThirdPartyCollectionChanged -= SetThirdPartCollectionIds; |
| 63 | 102 | | backpackFiltersController.OnSortByChanged -= SetSorting; |
| 63 | 103 | | backpackFiltersController.OnSearchTextChanged -= SetNameFilterFromSearchText; |
| 63 | 104 | | backpackFiltersController.OnCollectionTypeChanged -= SetCollectionTypeFromFilterSelection; |
| 63 | 105 | | backpackFiltersController.Dispose(); |
| | 106 | |
|
| 63 | 107 | | avatarSlotsHUDController.OnToggleSlot -= SetCategoryFromFilterSelection; |
| 63 | 108 | | avatarSlotsHUDController.Dispose(); |
| | 109 | |
|
| 63 | 110 | | view.Dispose(); |
| 63 | 111 | | requestWearablesCancellationToken.SafeCancelAndDispose(); |
| 63 | 112 | | filtersCancellationToken.SafeCancelAndDispose(); |
| 63 | 113 | | } |
| | 114 | |
|
| | 115 | | public void LoadWearables() |
| | 116 | | { |
| 14 | 117 | | LoadWearablesWithFilters(categoryFilter, collectionTypeMask, thirdPartyCollectionIdsFilter, |
| | 118 | | nameFilter, wearableSorting); |
| 14 | 119 | | } |
| | 120 | |
|
| | 121 | | public void LoadWearablesWithFilters(string categoryFilter = null, |
| | 122 | | NftCollectionType collectionTypeMask = NftCollectionType.All, |
| | 123 | | ICollection<string> thirdPartyCollectionIdsFilter = null, string nameFilter = null, |
| | 124 | | (NftOrderByOperation type, bool directionAscendent)? wearableSorting = null) |
| | 125 | | { |
| 38 | 126 | | this.categoryFilter = categoryFilter; |
| 38 | 127 | | this.collectionTypeMask = collectionTypeMask; |
| 38 | 128 | | this.thirdPartyCollectionIdsFilter = thirdPartyCollectionIdsFilter; |
| 38 | 129 | | this.nameFilter = nameFilter; |
| 38 | 130 | | this.wearableSorting = wearableSorting; |
| 38 | 131 | | requestWearablesCancellationToken = requestWearablesCancellationToken.SafeRestart(); |
| 38 | 132 | | ShowWearablesAndUpdateFilters(1, requestWearablesCancellationToken.Token).Forget(); |
| 38 | 133 | | } |
| | 134 | |
|
| | 135 | | public void CancelWearableLoading() => |
| 30 | 136 | | requestWearablesCancellationToken.SafeCancelAndDispose(); |
| | 137 | |
|
| | 138 | | public void Equip(string wearableId) |
| | 139 | | { |
| 29 | 140 | | if (!currentWearables.TryGetValue(wearableId, out WearableGridItemModel wearableGridModel)) |
| 28 | 141 | | return; |
| | 142 | |
|
| 1 | 143 | | wearableGridModel.IsEquipped = true; |
| 1 | 144 | | view.SetWearable(wearableGridModel); |
| 1 | 145 | | view.RefreshAllWearables(); |
| 1 | 146 | | } |
| | 147 | |
|
| | 148 | | public void UnEquip(string wearableId) |
| | 149 | | { |
| 9 | 150 | | if (!currentWearables.TryGetValue(wearableId, out WearableGridItemModel wearableGridModel)) |
| 8 | 151 | | return; |
| | 152 | |
|
| 1 | 153 | | wearableGridModel.IsEquipped = false; |
| 1 | 154 | | view.SetWearable(wearableGridModel); |
| 1 | 155 | | view.RefreshWearable(wearableId); |
| 1 | 156 | | } |
| | 157 | |
|
| | 158 | | public void UpdateBodyShapeCompatibility(string bodyShapeId) |
| | 159 | | { |
| 20 | 160 | | foreach ((string wearableId, WearableGridItemModel model) in currentWearables) |
| | 161 | | { |
| 2 | 162 | | if (!wearablesCatalogService.WearablesCatalog.TryGetValue(wearableId, out WearableItem wearable)) contin |
| 2 | 163 | | bool isCompatibleWithBodyShape = IsCompatibleWithBodyShape(bodyShapeId, wearable); |
| 2 | 164 | | model.IsCompatibleWithBodyShape = isCompatibleWithBodyShape; |
| 2 | 165 | | view.SetWearable(model); |
| | 166 | | } |
| 8 | 167 | | } |
| | 168 | |
|
| | 169 | | public void LoadCollections() => |
| 9 | 170 | | backpackFiltersController.LoadCollections(); |
| | 171 | |
|
| | 172 | | public void ResetFilters() |
| | 173 | | { |
| 30 | 174 | | categoryFilter = null; |
| 30 | 175 | | collectionTypeMask = NftCollectionType.Base | NftCollectionType.OnChain; |
| 30 | 176 | | thirdPartyCollectionIdsFilter = null; |
| 30 | 177 | | nameFilter = null; |
| 30 | 178 | | wearableSorting = new (NftOrderByOperation.Date, false); |
| 30 | 179 | | } |
| | 180 | |
|
| | 181 | | private async UniTaskVoid ShowWearablesAndUpdateFilters(int page, CancellationToken cancellationToken) |
| | 182 | | { |
| 46 | 183 | | List<(string reference, string name, string type, bool removable)> path = new (); |
| | 184 | |
|
| 46 | 185 | | var additiveReferencePath = $"{ALL_FILTER_REF}"; |
| 46 | 186 | | path.Add((reference: additiveReferencePath, name: "All", type: "all", removable: false)); |
| | 187 | |
|
| 46 | 188 | | if (!string.IsNullOrEmpty(categoryFilter)) |
| | 189 | | { |
| 6 | 190 | | additiveReferencePath += $"&{CATEGORY_FILTER_REF}{categoryFilter}"; |
| | 191 | |
|
| | 192 | | // TODO: translate category id into names (??) |
| 6 | 193 | | path.Add((reference: additiveReferencePath, name: categoryFilter, type: categoryFilter, removable: true) |
| | 194 | | } |
| | 195 | |
|
| 46 | 196 | | if (!string.IsNullOrEmpty(nameFilter)) |
| | 197 | | { |
| 5 | 198 | | additiveReferencePath += $"&{NAME_FILTER_REF}{nameFilter}"; |
| 5 | 199 | | path.Add((reference: additiveReferencePath, name: nameFilter, type: "nft-name", removable: true)); |
| | 200 | | } |
| | 201 | |
|
| 46 | 202 | | var wearableBreadcrumbModel = new NftBreadcrumbModel |
| | 203 | | { |
| | 204 | | Path = path.ToArray(), |
| | 205 | | Current = path.Count - 1, |
| | 206 | | ResultCount = 0, |
| | 207 | | }; |
| | 208 | |
|
| 46 | 209 | | view.SetWearableBreadcrumb(wearableBreadcrumbModel); |
| | 210 | |
|
| 46 | 211 | | if (string.IsNullOrEmpty(categoryFilter)) |
| | 212 | | { |
| 40 | 213 | | avatarSlotsHUDController.ClearSlotSelection(); |
| 40 | 214 | | OnCategoryFilterRemoved?.Invoke(); |
| | 215 | | } |
| | 216 | | else |
| 6 | 217 | | avatarSlotsHUDController.SelectSlot(categoryFilter, false); |
| | 218 | |
|
| 46 | 219 | | if (string.IsNullOrEmpty(nameFilter)) |
| 41 | 220 | | backpackFiltersController.ClearTextSearch(false); |
| | 221 | | else |
| 5 | 222 | | backpackFiltersController.SetTextSearch(nameFilter, false); |
| | 223 | |
|
| 46 | 224 | | if (wearableSorting != null) |
| | 225 | | { |
| 23 | 226 | | (NftOrderByOperation type, bool directionAscending) = wearableSorting.Value; |
| 23 | 227 | | backpackFiltersController.SetSorting(type, directionAscending, false); |
| | 228 | | } |
| | 229 | |
|
| 46 | 230 | | backpackFiltersController.SelectCollections(collectionTypeMask, thirdPartyCollectionIdsFilter, false); |
| | 231 | |
|
| 46 | 232 | | int resultCount = await RequestWearablesAndShowThem(page, cancellationToken); |
| | 233 | |
|
| | 234 | | // This call has been removed to avoid flickering. The result count is hidden in the view |
| | 235 | | // view.SetWearableBreadcrumb(wearableBreadcrumbModel with { ResultCount = resultCount }); |
| 46 | 236 | | } |
| | 237 | |
|
| | 238 | | private void HandleNewPageRequested(int page) |
| | 239 | | { |
| 1 | 240 | | requestWearablesCancellationToken = requestWearablesCancellationToken.SafeRestart(); |
| 1 | 241 | | RequestWearablesAndShowThem(page, requestWearablesCancellationToken.Token).Forget(); |
| 1 | 242 | | } |
| | 243 | |
|
| | 244 | | private async UniTask<int> RequestWearablesAndShowThem(int page, CancellationToken cancellationToken) |
| | 245 | | { |
| 47 | 246 | | AudioScriptableObjects.listItemAppear.ResetPitch(); |
| 47 | 247 | | UserProfile ownUserProfile = userProfileBridge.GetOwn(); |
| 47 | 248 | | string ownUserId = ownUserProfile.userId; |
| | 249 | |
|
| | 250 | | try |
| | 251 | | { |
| 47 | 252 | | currentWearables.Clear(); |
| | 253 | |
|
| 47 | 254 | | view.SetLoadingActive(true); |
| | 255 | |
|
| 47 | 256 | | List<WearableItem> wearables = new (); |
| | 257 | |
|
| 47 | 258 | | (IReadOnlyList<WearableItem> ownedWearables, int totalAmount) = await wearablesCatalogService.RequestOwn |
| | 259 | | ownUserId, |
| | 260 | | page, |
| | 261 | | PAGE_SIZE, cancellationToken, |
| | 262 | | categoryFilter, NftRarity.None, collectionTypeMask, |
| | 263 | | thirdPartyCollectionIdsFilter, |
| | 264 | | nameFilter, wearableSorting); |
| | 265 | |
|
| 47 | 266 | | wearables.AddRange(ownedWearables); |
| | 267 | |
|
| | 268 | | try |
| | 269 | | { |
| 47 | 270 | | totalAmount += await MergePublishedWearableCollections(page, wearables, totalAmount, cancellationTok |
| 47 | 271 | | totalAmount += await MergeBuilderWearableCollections(page, wearables, totalAmount, cancellationToken |
| 47 | 272 | | totalAmount += await MergeCustomWearableItems(page, wearables, totalAmount, cancellationToken); |
| | 273 | |
|
| 47 | 274 | | customWearablesBuffer.Clear(); |
| 47 | 275 | | } |
| 0 | 276 | | catch (Exception e) when (e is not OperationCanceledException) { Debug.LogError(e); } |
| | 277 | |
|
| 47 | 278 | | view.SetLoadingActive(false); |
| | 279 | |
|
| 47 | 280 | | currentWearables = wearables.Select(ToWearableGridModel) |
| 42 | 281 | | .ToDictionary(item => ExtendedUrnParser.GetShortenedUrn(item.WearableId), mo |
| | 282 | |
|
| 47 | 283 | | view.SetWearablePages(page, Mathf.CeilToInt((float)totalAmount / PAGE_SIZE)); |
| | 284 | |
|
| | 285 | | // TODO: mark the wearables to be disposed if no references left |
| 47 | 286 | | view.ClearWearables(); |
| 47 | 287 | | view.ShowWearables(currentWearables.Values); |
| | 288 | |
|
| 47 | 289 | | return totalAmount; |
| | 290 | | } |
| 0 | 291 | | catch (OperationCanceledException) { } |
| 0 | 292 | | catch (Exception e) { Debug.LogException(e); } |
| | 293 | |
|
| 0 | 294 | | return 0; |
| 47 | 295 | | } |
| | 296 | |
|
| | 297 | | private async UniTask<int> MergeCustomWearableItems(int page, List<WearableItem> wearables, |
| | 298 | | int totalAmount, CancellationToken cancellationToken) => |
| 47 | 299 | | await MergeToWearableResults(page, wearables, totalAmount, FetchCustomWearableItems, cancellationToken); |
| | 300 | |
|
| | 301 | | private async UniTask<int> MergePublishedWearableCollections(int page, List<WearableItem> wearables, int totalAm |
| | 302 | | CancellationToken cancellationToken) => |
| 47 | 303 | | await MergeToWearableResults(page, wearables, totalAmount, FetchPublishedWearableCollections, cancellationT |
| | 304 | |
|
| | 305 | | private async UniTask<int> MergeToWearableResults(int page, List<WearableItem> wearables, int totalAmount, |
| | 306 | | Func<List<WearableItem>, CancellationToken, UniTask> fetchOperation, |
| | 307 | | CancellationToken cancellationToken) |
| | 308 | | { |
| 94 | 309 | | int startingPage = (totalAmount / PAGE_SIZE) + 1; |
| 94 | 310 | | int pageOffset = page - startingPage; |
| 94 | 311 | | int pageSize = PAGE_SIZE - wearables.Count; |
| 94 | 312 | | int skip = pageOffset * PAGE_SIZE; |
| 94 | 313 | | int until = skip + pageSize; |
| | 314 | |
|
| 94 | 315 | | customWearablesBuffer.Clear(); |
| | 316 | |
|
| 94 | 317 | | await fetchOperation.Invoke(customWearablesBuffer, cancellationToken); |
| | 318 | |
|
| 106 | 319 | | if (skip < 0) return customWearablesBuffer.Count; |
| | 320 | |
|
| 166 | 321 | | for (int i = skip; i < customWearablesBuffer.Count && i < until; i++) |
| 1 | 322 | | wearables.Add(customWearablesBuffer[i]); |
| | 323 | |
|
| 82 | 324 | | return customWearablesBuffer.Count; |
| 94 | 325 | | } |
| | 326 | |
|
| | 327 | | private async UniTask<int> MergeBuilderWearableCollections(int page, List<WearableItem> wearables, int totalAmou |
| | 328 | | { |
| 47 | 329 | | int startingPage = (totalAmount / PAGE_SIZE) + 1; |
| 47 | 330 | | int pageOffset = Mathf.Max(1, page - startingPage); |
| 47 | 331 | | int pageSize = PAGE_SIZE - wearables.Count; |
| | 332 | |
|
| 47 | 333 | | customWearablesBuffer.Clear(); |
| | 334 | |
|
| 47 | 335 | | int collectionsWearableCount = await FetchBuilderWearableCollections(pageOffset, PAGE_SIZE, |
| | 336 | | customWearablesBuffer, cancellationToken); |
| | 337 | |
|
| 96 | 338 | | for (var i = 0; i < pageSize && i < customWearablesBuffer.Count; i++) |
| 1 | 339 | | wearables.Add(customWearablesBuffer[i]); |
| | 340 | |
|
| 47 | 341 | | return collectionsWearableCount; |
| 47 | 342 | | } |
| | 343 | |
|
| | 344 | | private async UniTask FetchCustomWearableItems(ICollection<WearableItem> wearables, CancellationToken cancellati |
| | 345 | | { |
| 47 | 346 | | IReadOnlyList<string> customItems = await customNftCollectionService.GetConfiguredCustomNftItemsAsync(cancel |
| | 347 | |
|
| 47 | 348 | | WearableItem[] retrievedWearables = await UniTask.WhenAll(customItems.Select(nftId => |
| | 349 | | { |
| 0 | 350 | | if (nftId.StartsWith("urn", StringComparison.OrdinalIgnoreCase)) |
| 0 | 351 | | return wearablesCatalogService.RequestWearableAsync(nftId, cancellationToken); |
| | 352 | |
|
| 0 | 353 | | return wearablesCatalogService.RequestWearableFromBuilderAsync(nftId, cancellationToken); |
| | 354 | | })); |
| | 355 | |
|
| 94 | 356 | | foreach (WearableItem wearable in retrievedWearables) |
| | 357 | | { |
| 0 | 358 | | if (wearable == null) |
| | 359 | | { |
| 0 | 360 | | Debug.LogWarning("Custom wearable item skipped is null"); |
| 0 | 361 | | continue; |
| | 362 | | } |
| | 363 | |
|
| 0 | 364 | | wearables.Add(wearable); |
| | 365 | | } |
| 47 | 366 | | } |
| | 367 | |
|
| | 368 | | private async UniTask FetchPublishedWearableCollections( |
| | 369 | | List<WearableItem> wearableBuffer, CancellationToken cancellationToken) |
| | 370 | | { |
| 47 | 371 | | IReadOnlyList<string> customCollections = |
| | 372 | | await customNftCollectionService.GetConfiguredCustomNftCollectionAsync(cancellationToken); |
| | 373 | |
|
| 47 | 374 | | HashSet<string> collectionsToRequest = HashSetPool<string>.Get(); |
| | 375 | |
|
| 98 | 376 | | foreach (string collectionId in customCollections) |
| 2 | 377 | | if (collectionId.StartsWith("urn", StringComparison.OrdinalIgnoreCase)) |
| 1 | 378 | | collectionsToRequest.Add(collectionId); |
| | 379 | |
|
| 47 | 380 | | await wearablesCatalogService.RequestWearableCollection(collectionsToRequest, cancellationToken, wearableBuf |
| | 381 | |
|
| 47 | 382 | | HashSetPool<string>.Release(collectionsToRequest); |
| 47 | 383 | | } |
| | 384 | |
|
| | 385 | | private async UniTask<int> FetchBuilderWearableCollections( |
| | 386 | | int pageNumber, int pageSize, |
| | 387 | | List<WearableItem> wearableBuffer, |
| | 388 | | CancellationToken cancellationToken) |
| | 389 | | { |
| 47 | 390 | | IReadOnlyList<string> customCollections = |
| | 391 | | await customNftCollectionService.GetConfiguredCustomNftCollectionAsync(cancellationToken); |
| | 392 | |
|
| 47 | 393 | | HashSet<string> collectionsToRequest = HashSetPool<string>.Get(); |
| | 394 | |
|
| 98 | 395 | | foreach (string collectionId in customCollections) |
| 2 | 396 | | if (!collectionId.StartsWith("urn", StringComparison.OrdinalIgnoreCase)) |
| 1 | 397 | | collectionsToRequest.Add(collectionId); |
| | 398 | |
|
| 47 | 399 | | (IReadOnlyList<WearableItem> _, int totalAmount) = await wearablesCatalogService.RequestWearableCollectionIn |
| | 400 | | collectionsToRequest, cancellationToken, |
| | 401 | | collectionBuffer: wearableBuffer, |
| | 402 | | nameFilter: nameFilter, |
| | 403 | | pageNumber: pageNumber, pageSize: pageSize); |
| | 404 | |
|
| 47 | 405 | | HashSetPool<string>.Release(collectionsToRequest); |
| | 406 | |
|
| 47 | 407 | | return totalAmount; |
| 47 | 408 | | } |
| | 409 | |
|
| | 410 | | private WearableGridItemModel ToWearableGridModel(WearableItem wearable) |
| | 411 | | { |
| | 412 | | NftRarity rarity; |
| | 413 | |
|
| 21 | 414 | | if (string.IsNullOrEmpty(wearable.rarity)) |
| 0 | 415 | | rarity = NftRarity.None; |
| 21 | 416 | | else if (!Enum.TryParse(wearable.rarity, true, out NftRarity result)) |
| | 417 | | { |
| 0 | 418 | | rarity = NftRarity.None; |
| 0 | 419 | | Debug.LogWarning($"Could not parse the rarity \"{wearable.rarity}\" of the wearable '{wearable.id}'. Fal |
| | 420 | | } |
| | 421 | | else |
| 21 | 422 | | rarity = result; |
| | 423 | |
|
| 21 | 424 | | string currentBodyShapeId = dataStoreBackpackV2.previewBodyShape.Get(); |
| | 425 | |
|
| 21 | 426 | | return new WearableGridItemModel |
| | 427 | | { |
| | 428 | | WearableId = wearable.id, |
| | 429 | | Rarity = rarity, |
| | 430 | | Category = wearable.data.category, |
| | 431 | | ImageUrl = wearable.ComposeThumbnailUrl(), |
| | 432 | | IsEquipped = IsEquipped(ExtendedUrnParser.GetShortenedUrn(wearable.id)), |
| | 433 | | IsNew = (DateTime.UtcNow - wearable.MostRecentTransferredDate).TotalHours < 24, |
| | 434 | | IsSelected = false, |
| | 435 | | UnEquipAllowed = wearable.CanBeUnEquipped(), |
| | 436 | | IsCompatibleWithBodyShape = IsCompatibleWithBodyShape(currentBodyShapeId, wearable), |
| | 437 | | IsSmartWearable = wearable.IsSmart(), |
| | 438 | | Amount = wearable.amount > 1 ? $"x{wearable.amount.ToString()}" : "", |
| | 439 | | }; |
| | 440 | | } |
| | 441 | |
|
| | 442 | | private bool IsEquipped(string wearableId) => |
| 22 | 443 | | dataStoreBackpackV2.previewEquippedWearables.Contains(wearableId) |
| | 444 | | || wearableId == dataStoreBackpackV2.previewBodyShape.Get(); |
| | 445 | |
|
| | 446 | | private void HandleWearableSelected(WearableGridItemModel wearableGridItem) |
| | 447 | | { |
| 1 | 448 | | string wearableId = wearableGridItem.WearableId; |
| 1 | 449 | | string shortenedWearableId = ExtendedUrnParser.GetShortenedUrn(wearableId); |
| | 450 | |
|
| 1 | 451 | | view.ClearWearableSelection(); |
| 1 | 452 | | view.SelectWearable(shortenedWearableId); |
| | 453 | |
|
| 1 | 454 | | if (!wearablesCatalogService.WearablesCatalog.TryGetValue(wearableId, out WearableItem wearable)) |
| | 455 | | { |
| 0 | 456 | | Debug.LogError($"Cannot fill the wearable info card, the wearable id does not exist {wearableId}"); |
| 0 | 457 | | return; |
| | 458 | | } |
| | 459 | |
|
| 1 | 460 | | string[] hidesList = wearable.GetHidesList(userProfileBridge.GetOwn().avatar.bodyShape); |
| | 461 | |
|
| 1 | 462 | | view.FillInfoCard(new InfoCardComponentModel |
| | 463 | | { |
| | 464 | | rarity = wearable.rarity, |
| | 465 | | category = wearable.data.category, |
| | 466 | | description = string.IsNullOrEmpty(wearable.description) ? EMPTY_WEARABLE_DESCRIPTION : wearable.descrip |
| | 467 | | imageUri = wearable.ComposeThumbnailUrl(), |
| | 468 | |
|
| | 469 | | // TODO: solve hidden by field |
| | 470 | | hiddenBy = null, |
| | 471 | | name = wearable.GetName(), |
| | 472 | | hideList = hidesList != null ? hidesList.ToList() : new List<string>(), |
| | 473 | | isEquipped = IsEquipped(shortenedWearableId), |
| | 474 | | removeList = wearable.data.replaces != null ? wearable.data.replaces.ToList() : new List<string>(), |
| | 475 | | wearableId = wearableId, |
| | 476 | | unEquipAllowed = wearable.CanBeUnEquipped(), |
| | 477 | | blockVrmExport = wearable.data.blockVrmExport, |
| | 478 | | }); |
| | 479 | |
|
| 1 | 480 | | OnWearableSelected?.Invoke(wearableId); |
| 1 | 481 | | } |
| | 482 | |
|
| | 483 | | private void HandleWearableUnequipped(WearableGridItemModel wearableGridItem, UnequipWearableSource source) => |
| 3 | 484 | | OnWearableUnequipped?.Invoke(wearableGridItem.WearableId, source); |
| | 485 | |
|
| | 486 | | private void HandleWearableEquipped(WearableGridItemModel wearableGridItem, EquipWearableSource source) => |
| 13 | 487 | | OnWearableEquipped?.Invoke(wearableGridItem.WearableId, source); |
| | 488 | |
|
| | 489 | | private void FilterWearablesFromReferencePath(string referencePath) |
| | 490 | | { |
| 4 | 491 | | string[] filters = referencePath.Split('&', StringSplitOptions.RemoveEmptyEntries); |
| | 492 | |
|
| 4 | 493 | | nameFilter = null; |
| 4 | 494 | | categoryFilter = null; |
| | 495 | |
|
| 28 | 496 | | foreach (string filter in filters) |
| | 497 | | { |
| 10 | 498 | | if (filter.StartsWith(NAME_FILTER_REF)) |
| 3 | 499 | | nameFilter = filter[5..]; |
| 7 | 500 | | else if (filter.StartsWith(CATEGORY_FILTER_REF)) |
| 3 | 501 | | categoryFilter = filter[9..]; |
| | 502 | | } |
| | 503 | |
|
| 4 | 504 | | requestWearablesCancellationToken = requestWearablesCancellationToken.SafeRestart(); |
| 4 | 505 | | ShowWearablesAndUpdateFilters(1, requestWearablesCancellationToken.Token).Forget(); |
| 4 | 506 | | } |
| | 507 | |
|
| | 508 | | private void RemoveFiltersFromReferencePath(string referencePath) |
| | 509 | | { |
| 4 | 510 | | string[] filters = referencePath.Split('&', StringSplitOptions.RemoveEmptyEntries); |
| 4 | 511 | | string filter = filters[^1]; |
| | 512 | |
|
| 4 | 513 | | if (filter.StartsWith(NAME_FILTER_REF)) |
| 2 | 514 | | nameFilter = null; |
| 2 | 515 | | else if (filter.StartsWith(CATEGORY_FILTER_REF)) |
| 2 | 516 | | categoryFilter = null; |
| | 517 | |
|
| 4 | 518 | | requestWearablesCancellationToken = requestWearablesCancellationToken.SafeRestart(); |
| 4 | 519 | | ShowWearablesAndUpdateFilters(1, requestWearablesCancellationToken.Token).Forget(); |
| 4 | 520 | | } |
| | 521 | |
|
| | 522 | | private void GoToMarketplace() |
| | 523 | | { |
| 2 | 524 | | browserBridge.OpenUrl(userProfileBridge.GetOwn().hasConnectedWeb3 |
| | 525 | | ? URL_MARKET_PLACE |
| | 526 | | : URL_GET_A_WALLET); |
| 2 | 527 | | } |
| | 528 | |
|
| | 529 | | private void SetThirdPartCollectionIds(HashSet<string> selectedCollections) |
| | 530 | | { |
| 0 | 531 | | thirdPartyCollectionIdsFilter = selectedCollections; |
| 0 | 532 | | filtersCancellationToken = filtersCancellationToken.SafeRestart(); |
| 0 | 533 | | ThrottleLoadWearablesWithCurrentFilters(filtersCancellationToken.Token).Forget(); |
| 0 | 534 | | view.SetInfoCardVisible(false); |
| 0 | 535 | | } |
| | 536 | |
|
| | 537 | | private void SetSorting((NftOrderByOperation type, bool directionAscendent) newSorting) |
| | 538 | | { |
| 1 | 539 | | wearableSorting = newSorting; |
| 1 | 540 | | filtersCancellationToken = filtersCancellationToken.SafeRestart(); |
| 1 | 541 | | ThrottleLoadWearablesWithCurrentFilters(filtersCancellationToken.Token).Forget(); |
| 1 | 542 | | view.SetInfoCardVisible(false); |
| 1 | 543 | | backpackAnalyticsService.SendWearableSortedBy(newSorting.type, newSorting.directionAscendent); |
| 1 | 544 | | } |
| | 545 | |
|
| | 546 | | private void SetNameFilterFromSearchText(string newText) |
| | 547 | | { |
| 1 | 548 | | categoryFilter = null; |
| 1 | 549 | | collectionTypeMask = NftCollectionType.All; |
| 1 | 550 | | thirdPartyCollectionIdsFilter?.Clear(); |
| 1 | 551 | | nameFilter = newText; |
| 1 | 552 | | filtersCancellationToken = filtersCancellationToken.SafeRestart(); |
| 1 | 553 | | ThrottleLoadWearablesWithCurrentFilters(filtersCancellationToken.Token).Forget(); |
| 1 | 554 | | view.SetInfoCardVisible(false); |
| 1 | 555 | | backpackAnalyticsService.SendWearableSearch(newText); |
| 1 | 556 | | } |
| | 557 | |
|
| | 558 | | private void SetCollectionTypeFromFilterSelection(NftCollectionType collectionType) |
| | 559 | | { |
| 0 | 560 | | nameFilter = null; |
| 0 | 561 | | collectionTypeMask = collectionType; |
| 0 | 562 | | filtersCancellationToken = filtersCancellationToken.SafeRestart(); |
| 0 | 563 | | ThrottleLoadWearablesWithCurrentFilters(filtersCancellationToken.Token).Forget(); |
| 0 | 564 | | view.SetInfoCardVisible(false); |
| 0 | 565 | | backpackAnalyticsService.SendWearableFilter(!collectionType.HasFlag(NftCollectionType.Base)); |
| 0 | 566 | | } |
| | 567 | |
|
| | 568 | | private void SetCategoryFromFilterSelection(string category, bool supportColor, PreviewCameraFocus previewCamera |
| | 569 | | { |
| 5 | 570 | | nameFilter = null; |
| 5 | 571 | | SetCategory(category, isSelected); |
| 5 | 572 | | } |
| | 573 | |
|
| | 574 | | private void SetCategory(string category, bool isSelected) |
| | 575 | | { |
| 5 | 576 | | categoryFilter = isSelected ? category : null; |
| 5 | 577 | | filtersCancellationToken = filtersCancellationToken.SafeRestart(); |
| 5 | 578 | | ThrottleLoadWearablesWithCurrentFilters(filtersCancellationToken.Token).Forget(); |
| 5 | 579 | | view.SetInfoCardVisible(false); |
| 5 | 580 | | } |
| | 581 | |
|
| | 582 | | private async UniTaskVoid ThrottleLoadWearablesWithCurrentFilters(CancellationToken cancellationToken) |
| | 583 | | { |
| 21 | 584 | | await UniTask.NextFrame(cancellationToken); |
| 3 | 585 | | LoadWearables(); |
| 3 | 586 | | } |
| | 587 | |
|
| | 588 | | private bool IsCompatibleWithBodyShape(string bodyShapeId, WearableItem wearable) |
| | 589 | | { |
| 23 | 590 | | bool isCompatibleWithBodyShape = wearable.data.category |
| | 591 | | is WearableLiterals.Categories.BODY_SHAPE |
| | 592 | | or WearableLiterals.Categories.SKIN |
| | 593 | | || wearable.SupportsBodyShape(bodyShapeId); |
| | 594 | |
|
| 7 | 595 | | return isCompatibleWithBodyShape; |
| | 596 | | } |
| | 597 | | } |
| | 598 | | } |