Error executing template "Designs/Swift/Paragraph/Swift_ProductDetailsImage_Custom.cshtml" System.NullReferenceException: Object reference not set to an instance of an object. at CompiledRazorTemplates.Dynamic.RazorEngine_562f88162e4c43f6b0f0db60df8fa558.<>c__DisplayClass30_1.<SortAssets>b__4(String f) in D:\dynamicweb.net\Solutions\twodayco3\evasolo.cloud.dynamicweb-cms.com\Files\Templates\Designs\Swift\Paragraph\Swift_ProductDetailsImage_Custom.cshtml:line 95 at System.Array.FindIndex[T](T[] array, Int32 startIndex, Int32 count, Predicate`1 match) at System.Array.FindIndex[T](T[] array, Predicate`1 match) at CompiledRazorTemplates.Dynamic.RazorEngine_562f88162e4c43f6b0f0db60df8fa558.<>c__DisplayClass30_0.<SortAssets>b__1(<>f__AnonymousType0`2 x) in D:\dynamicweb.net\Solutions\twodayco3\evasolo.cloud.dynamicweb-cms.com\Files\Templates\Designs\Swift\Paragraph\Swift_ProductDetailsImage_Custom.cshtml:line 95 at System.Linq.EnumerableSorter`2.ComputeKeys(TElement[] elements, Int32 count) at System.Linq.EnumerableSorter`1.Sort(TElement[] elements, Int32 count) at System.Linq.OrderedEnumerable`1.<GetEnumerator>d__1.MoveNext() at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext() at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection) at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source) at CompiledRazorTemplates.Dynamic.RazorEngine_562f88162e4c43f6b0f0db60df8fa558.SortAssets(IEnumerable`1 assetsList, String[] sortedFormats) in D:\dynamicweb.net\Solutions\twodayco3\evasolo.cloud.dynamicweb-cms.com\Files\Templates\Designs\Swift\Paragraph\Swift_ProductDetailsImage_Custom.cshtml:line 91 at CompiledRazorTemplates.Dynamic.RazorEngine_562f88162e4c43f6b0f0db60df8fa558.Execute() in D:\dynamicweb.net\Solutions\twodayco3\evasolo.cloud.dynamicweb-cms.com\Files\Templates\Designs\Swift\Paragraph\Swift_ProductDetailsImage_Custom.cshtml:line 162 at RazorEngine.Templating.TemplateBase.RazorEngine.Templating.ITemplate.Run(ExecuteContext context, TextWriter reader) at RazorEngine.Templating.RazorEngineService.RunCompile(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag) at RazorEngine.Templating.RazorEngineServiceExtensions.<>c__DisplayClass16_0.<RunCompile>b__0(TextWriter writer) at RazorEngine.Templating.RazorEngineServiceExtensions.WithWriter(Action`1 withWriter) at Dynamicweb.Rendering.RazorTemplateRenderingProvider.Render(Template template) at Dynamicweb.Rendering.TemplateRenderingService.Render(Template template) at Dynamicweb.Rendering.Template.RenderRazorTemplate()
1 @inherits Dynamicweb.Rendering.ViewModelTemplate<Dynamicweb.Frontend.ParagraphViewModel> 2 @using Dynamicweb.Core.Encoders @*//CUSTOM*@ 3 @using Dynamicweb.Ecommerce.ProductCatalog 4 @using Dynamicweb.Frontend 5 @using System.IO 6 7 @* CUSTOMIZED STANDARD SWIFT (v1.25.0) TEMPLATE *@ 8 9 @functions { 10 public ProductViewModel product { get; set; } = new ProductViewModel(); 11 public string galleryLayout { get; set; } 12 public string[] supportedImageFormats { get; set; } 13 public string[] supportedVideoFormats { get; set; } 14 public string[] supportedDocumentFormats { get; set; } 15 public string[] allSupportedFormats { get; set; } 16 17 public class RatioSettings 18 { 19 public string Ratio { get; set; } 20 public string CssClass { get; set; } 21 public string CssVariable { get; set; } 22 public string Fill { get; set; } 23 } 24 25 public RatioSettings GetRatioSettings(string size = "desktop") 26 { 27 var ratioSettings = new RatioSettings(); 28 29 string ratio = Model.Item.GetRawValueString("ImageAspectRatio", ""); 30 ratio = ratio != "0" ? ratio : ""; 31 string cssClass = ratio != "" && ratio != "fill" ? " ratio" : ""; 32 string cssVariable = ratio != "" && ratio != "fill" ? "--bs-aspect-ratio: " + ratio : ""; 33 cssClass = ratio == "fill" && size == "mobile" ? " ratio" : cssClass; 34 cssVariable = ratio == "fill" && size == "mobile" ? "--bs-aspect-ratio: 66%" : cssVariable; 35 36 ratioSettings.Ratio = ratio; 37 ratioSettings.CssClass = cssClass; 38 ratioSettings.CssVariable = cssVariable; 39 ratioSettings.Fill = ratio == "fill" ? " h-100" : ""; 40 41 return ratioSettings; 42 } 43 44 public string GetArrowsColor() 45 { 46 var invertColor = Model.Item.GetBoolean("InvertModalArrowsColor"); 47 var arrowsColor = invertColor ? " carousel-dark" : string.Empty; 48 return arrowsColor; 49 } 50 51 public string GetThumbnailPlacement() 52 { 53 return Model.Item.GetRawValueString("ThumbnailPlacement", "bottom"); 54 } 55 56 public string GetThumbnailRowSettingCss() 57 { 58 switch (GetThumbnailPlacement()) 59 { 60 case "bottom": 61 return "custom-productdetailsimage__position-bottom"; //CUSTOM 62 case "left": 63 return "order-first"; //CUSTOM 64 case "right": 65 return "order-last"; //CUSTOM 66 default: 67 return ""; //CUSTOM 68 } 69 } 70 71 //CUSTOM 72 public string GetThumbnailRowSettingCssWrapper() 73 { 74 switch (GetThumbnailPlacement()) 75 { 76 case "bottom": 77 return "d-flex"; 78 case "left": 79 return "d-flex flex-column"; 80 case "right": 81 return "d-flex flex-column"; 82 default: 83 return "d-flex flex-wrap"; 84 } 85 } 86 //--CUSTOM 87 88 //CUSTOM 89 public IEnumerable<MediaViewModel> SortAssets(IEnumerable<MediaViewModel> assetsList, string[] sortedFormats) 90 { 91 return assetsList 92 .Select((asset, i) => new { asset, originalIndex = i }) 93 .OrderBy(x => 94 { 95 var index = Array.FindIndex(sortedFormats, f => x.asset.DisplayName.StartsWith(f)); 96 return index == -1 ? int.MaxValue : index; 97 }) 98 .ThenBy(x => x.originalIndex) 99 .Select(x => x.asset) 100 .ToList(); 101 } 102 //--CUSTOM 103 104 //CUSTOM 105 public static string GetAspectRatio(string value) 106 { 107 switch (value) 108 { 109 case "0": return "auto"; 110 case "fill": return "cover"; 111 case "100%": return "1 / 1"; 112 case "56%": return "16 / 9"; 113 case "177%": return "9 / 16"; 114 case "75%": return "4 / 3"; 115 case "133%": return "3 / 4"; 116 case "28%": return "32 / 9"; 117 default: return "auto"; 118 } 119 } 120 //--CUSTOM 121 } 122 123 @{ 124 ProductViewModel product = null; 125 if (Dynamicweb.Context.Current.Items.Contains("ProductDetails")) 126 { 127 product = (ProductViewModel)Dynamicweb.Context.Current.Items["ProductDetails"]; 128 } 129 else if (Pageview.Page.Item["DummyProduct"] != null && Pageview.IsVisualEditorMode) 130 { 131 var pageViewModel = Dynamicweb.Frontend.ContentViewModelFactory.CreatePageInfoViewModel(Pageview.Page); 132 ProductListViewModel productList = pageViewModel.Item.GetValue("DummyProduct") != null ? pageViewModel.Item.GetValue("DummyProduct") as ProductListViewModel : new ProductListViewModel(); 133 134 if (productList?.Products is object) 135 { 136 product = productList.Products[0]; 137 } 138 } 139 } 140 141 @if (product is object) 142 { 143 @* Supported formats *@ 144 supportedImageFormats = new string[] { ".jpg", ".jpeg", ".webp", ".png", ".gif", ".bmp", ".tiff" }; 145 supportedVideoFormats = new string[] { "youtu.be", "youtube", "vimeo", ".mp4", ".webm" }; 146 supportedDocumentFormats = new string[] { ".pdf", ".docx", ".xlsx", ".ppt", "pptx" }; 147 allSupportedFormats = supportedImageFormats.Concat(supportedVideoFormats).Concat(supportedDocumentFormats).ToArray(); 148 149 @* Collect the assets *@ 150 var selectedAssetCategories = Model.Item.GetList("ImageAssets")?.GetRawValue().OfType<string>(); 151 var selectedAssetCategoriesSort = Pageview.AreaSettings.GetItem("CustomSettings").GetRawValueString("AssetCategoriesSort").Split(','); //CUSTOM 152 bool includeImagePatternImages = Model.Item.GetBoolean("ImagePatternImages"); 153 154 @* Needed image data collection to support both DefaultImage, ImagePatterns and Image Assets *@ 155 string defaultImage = product.DefaultImage != null ? product.DefaultImage.Value : ""; 156 IEnumerable<MediaViewModel> assetsImages = product.AssetCategories.Where(x => selectedAssetCategories.Contains(x.SystemName)).SelectMany(x => x.Assets); 157 assetsImages = assetsImages.OrderByDescending(x => x.Value.Equals(defaultImage)); 158 IEnumerable<MediaViewModel> assetsList = new MediaViewModel[] { }; 159 assetsList = assetsList.Union(assetsImages); 160 assetsList = includeImagePatternImages ? assetsList.Union(product.ImagePatternImages) : assetsList; 161 assetsList = includeImagePatternImages && assetsList.Count() == 0 ? assetsList.Append(product.DefaultImage) : assetsList; 162 assetsList = selectedAssetCategoriesSort.Any() ? SortAssets(assetsList, selectedAssetCategoriesSort) : assetsList; //CUSTOM 163 164 bool defaultImageFallback = Model.Item.GetBoolean("DefaultImageFallback"); 165 bool showOnlyPrimaryImage = Model.Item.GetBoolean("ShowOnlyPrimaryImage"); 166 167 int totalAssets = 0; 168 if (showOnlyPrimaryImage == false) 169 { 170 foreach (MediaViewModel asset in assetsList) 171 { 172 var assetValue = asset.Value; 173 foreach (string format in allSupportedFormats) 174 { 175 if (assetValue.IndexOf(format, StringComparison.OrdinalIgnoreCase) >= 0) 176 { 177 totalAssets++; 178 } 179 } 180 } 181 } 182 183 if ((totalAssets == 0 && product.DefaultImage != null && selectedAssetCategories.Count() == 0) || (showOnlyPrimaryImage == true && product.DefaultImage != null) || totalAssets == 0 && defaultImageFallback) 184 { 185 assetsList = new List<MediaViewModel>() { product.DefaultImage }; 186 totalAssets = 1; 187 } 188 189 @* Theme settings *@ 190 string theme = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("Theme")) ? " theme " + Model.Item.GetRawValueString("Theme").Replace(" ", "").Trim().ToLower() : ""; 191 192 var badgeParms = new Dictionary<string, object>(); 193 badgeParms.Add("size", "h5"); 194 badgeParms.Add("saleBadgeType", Model.Item.GetRawValue("SaleBadgeType")); 195 badgeParms.Add("saleBadgeCssClassName", Model.Item.GetRawValue("SaleBadgeDesign")); 196 badgeParms.Add("newBadgeCssClassName", Model.Item.GetRawValue("NewBadgeDesign")); 197 badgeParms.Add("newPublicationDays", Model.Item.GetInt32("NewPublicationDays")); 198 badgeParms.Add("campaignBadgesValues", Model.Item.GetRawValueString("CampaignBadges")); 199 200 bool saleBadgeEnabled = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("SaleBadgeDesign")) && Model.Item.GetRawValueString("SaleBadgeDesign") != "none" ? true : false; 201 bool newBadgeEnabled = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("NewBadgeDesign")) && Model.Item.GetRawValueString("NewBadgeDesign") != "none" ? true : false; 202 DateTime createdDate = product.Created.Value; 203 bool showBadges = saleBadgeEnabled && product.Discount.Price != 0 ? true : false; 204 showBadges = (newBadgeEnabled && Model.Item.GetInt32("NewPublicationDays") == 0) || (newBadgeEnabled && (createdDate.AddDays(Model.Item.GetInt32("NewPublicationDays")) > DateTime.Now)) ? true : showBadges; 205 showBadges = !string.IsNullOrEmpty(Model.Item.GetRawValueString("CampaignBadges")) ? true : showBadges; 206 207 //CUSTOM 208 var videoObjectFit = Model.Item.GetRawValueString("Custom_VideoObjectFit", "plyr__wrap-aspect"); 209 var videoControlsPosition = Model.Item.GetRawValueString("Custom_VideoControlsPosition", " ").Trim(); 210 videoControlsPosition = videoObjectFit == "plyr__wrap-aspect" && (videoControlsPosition == "plyr__wrap-controls") ? "" : videoControlsPosition; 211 videoControlsPosition = videoObjectFit == "plyr__wrap-cover" ? "plyr__wrap-nocontrols" : videoControlsPosition; 212 var videoAspectRatio = GetAspectRatio(Model.Item.GetRawValueString("ImageAspectRatio", "")); 213 videoAspectRatio = videoObjectFit == "plyr__wrap-aspect" ? "auto" : videoAspectRatio; 214 //--CUSTOM 215 216 @* Get assets from selected categories or get all assets *@ 217 if (totalAssets != 0) 218 { 219 int assetNumber = 0; 220 int thumbnailNumber = 0; 221 int modalAssetNumber = 0; 222 string thumbnailAxisCss = GetThumbnailPlacement() == "bottom" ? "flex-column" : string.Empty; 223 var useSwiffySlider = GetThumbnailPlacement() == "bottom"; //CUSTOM 224 225 <div class="d-flex h-100 @(thumbnailAxisCss) @(theme) item_@Model.Item.SystemName.ToLower() custom"> @*//CUSTOM*@ 226 <div id="SmallScreenImages_@Model.ID" class="carousel@(GetArrowsColor()) col position-relative" data-bs-ride="carousel"> 227 228 @*//CUSTOM*@ 229 @if (totalAssets > 1 && useSwiffySlider) 230 { 231 int indicatorIndex = 0; 232 233 <div class="carousel-counter">1 / @totalAssets</div> 234 <ol class="carousel-indicators"> 235 @foreach (MediaViewModel asset in assetsList) 236 { 237 <li data-bs-target="#SmallScreenImages_@Model.ID" data-bs-slide-to="@(indicatorIndex)" class="@(indicatorIndex == 0 ? "active" : null)"></li> 238 indicatorIndex++; 239 } 240 </ol> 241 } 242 @*//--CUSTOM*@ 243 244 <div class="carousel-inner h-100"> 245 @foreach (MediaViewModel asset in assetsList) 246 { 247 var assetValue = asset.Value; 248 foreach (string format in allSupportedFormats) 249 { 250 if (assetValue.IndexOf(format, StringComparison.OrdinalIgnoreCase) >= 0) 251 { 252 string activeSlide = assetNumber == 0 ? "active" : ""; 253 254 <div class="carousel-item @activeSlide" data-bs-interval="99999"> 255 @{ 256 string size = "mobile"; 257 258 string imageTheme = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("ImageTheme")) ? " theme " + Model.Item.GetRawValueString("ImageTheme").Replace(" ", "").Trim().ToLower() : ""; 259 260 261 <div class="h-100 @(imageTheme)"> 262 @foreach (string imageFormat in supportedImageFormats) 263 { //Images 264 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0) 265 { 266 if (product is object) 267 { 268 string productName = product.Name; 269 string imagePath = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value; 270 string imageLinkPath = Dynamicweb.Context.Current.Server.UrlEncode(imagePath); 271 272 RatioSettings ratioSettings = GetRatioSettings(size); 273 274 var parms = new Dictionary<string, object>(); 275 parms.Add("alt", productName + asset.Keywords); 276 parms.Add("itemprop", "image"); 277 parms.Add("columns", Model.GridRowColumnCount); 278 parms.Add("eagerLoadNewImages", Model.Item.GetBoolean("DisableLazyLoading")); 279 parms.Add("doNotUseGetimage", Model.Item.GetBoolean("DisableGetImage")); 280 if (!string.IsNullOrEmpty(asset.DisplayName)) 281 { 282 parms.Add("title", asset.DisplayName); 283 } 284 285 if (ratioSettings.Ratio == "fill" && galleryLayout != "grid") 286 { 287 parms.Add("cssClass", "w-100 h-100 image-zoom-lg-l-hover"); 288 } 289 else 290 { 291 parms.Add("cssClass", "mw-100 mh-100"); 292 } 293 294 <a href="@imageLinkPath" class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)" data-bs-toggle="modal" data-bs-target="#modal_@Model.ID"> 295 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide-to="@assetNumber"> 296 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms) 297 </div> 298 </a> 299 } 300 } 301 } 302 @foreach (string videoFormat in supportedVideoFormats) 303 { //Videos 304 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0) 305 { 306 if (Model.Item.GetString("OpenVideoInModal") == "true") 307 { 308 if (product is object) 309 { 310 string iconPath = "/Files/Templates/Designs/Swift/Assets/icons/"; 311 312 string videoScreendumpPath = !string.IsNullOrEmpty(asset.Value) ? asset.Value : ""; 313 string videoId = videoScreendumpPath.Substring(videoScreendumpPath.LastIndexOf('/') + 1); 314 videoScreendumpPath = videoScreendumpPath.IndexOf("youtu.be", StringComparison.OrdinalIgnoreCase) >= 0 || videoScreendumpPath.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) >= 0 ? "https://img.youtube.com/vi/" + videoId + "/maxresdefault.jpg" : videoScreendumpPath; 315 316 string vimeoJsClass = videoScreendumpPath.IndexOf("vimeo", StringComparison.OrdinalIgnoreCase) >= 0 ? "js-vimeo-video-thumbnail" : ""; 317 videoScreendumpPath = videoScreendumpPath.IndexOf("vimeo", StringComparison.OrdinalIgnoreCase) >= 0 ? "" : videoScreendumpPath; 318 319 string productName = product.Name; 320 productName += !string.IsNullOrEmpty(asset.Keywords) ? " " + asset.Keywords : ""; 321 string assetTitle = !string.IsNullOrEmpty(asset.DisplayName) ? "title=\"" + HtmlEncoder.HtmlAttributeEncode(asset.DisplayName) + "\"" : ""; //CUSTOM 322 323 RatioSettings ratioSettings = GetRatioSettings(size); 324 325 <div class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable); cursor: pointer" data-bs-toggle="modal" data-bs-target="#modal_@Model.ID"> 326 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide-to="@assetNumber"> 327 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(iconPath + "play-circle.svg")</div> 328 @if (videoScreendumpPath.IndexOf(".mp4", StringComparison.OrdinalIgnoreCase) < 0) 329 { 330 <img src="@videoScreendumpPath" loading="lazy" decoding="async" alt="@HtmlEncoder.HtmlAttributeEncode(productName)" @assetTitle class="@vimeoJsClass mw-100 mh-100" data-video-id="@videoId" style="object-fit: cover;" onload="CheckIfVideoThumbnailExist(this)"> @*//CUSTOM*@ 331 } 332 else 333 { 334 string videoType = Path.GetExtension(asset.Value).ToLower(); 335 string videoPathEncoded = System.Uri.EscapeDataString(asset.Value); 336 337 338 <video preload="auto" class="h-100 w-100" style="object-fit: contain;"> 339 <source src="@(videoPathEncoded)#t=0.001" type="video/@videoType.Replace(".", "")"> 340 </video> 341 } 342 </div> 343 </div> 344 345 <script> 346 function CheckIfVideoThumbnailExist(image) { 347 if (image.width == 120) { 348 const lowQualityImage = "https://img.youtube.com/vi/@(videoId)/hqdefault.jpg" 349 image.src = lowQualityImage; 350 } 351 } 352 </script> 353 } 354 } 355 else 356 { 357 if (product is object) 358 { 359 string assetName = !string.IsNullOrEmpty(asset.DisplayName) ? asset.DisplayName : asset.Name; 360 string videoId = asset.Value.Substring(asset.Value.LastIndexOf('/') + 1); 361 string type = assetValue.IndexOf("youtu.be", StringComparison.OrdinalIgnoreCase) >= 0 || assetValue.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) >= 0 ? "youtube" : ""; 362 type = assetValue.IndexOf("vimeo", StringComparison.OrdinalIgnoreCase) >= 0 ? "vimeo" : type; 363 type = assetValue.IndexOf(".mp4", StringComparison.OrdinalIgnoreCase) >= 0 || assetValue.IndexOf(".webm", StringComparison.OrdinalIgnoreCase) >= 0 ? "selfhosted" : type; 364 365 string openInModal = Model.Item.GetString("OpenVideoInModal"); 366 bool autoPlay = Model.Item.GetBoolean("VideoAutoPlay"); 367 368 // CUSTOM 369 370 <div class="d-block h-100" itemscope itemtype="https://schema.org/VideoObject"> 371 <span class="visually-hidden" itemprop="name">@assetName</span> 372 <span class="visually-hidden" itemprop="contentUrl">@asset.Value</span> 373 <span class="visually-hidden" itemprop="thumbnailUrl">@asset.Value</span> 374 375 @if (type != "selfhosted") 376 { 377 var playerId = $"player_{Pageview.CurrentParagraph.ID}_{videoId}_{size}"; 378 379 <div class="h-100 w-100 plyr__wrap @(videoObjectFit) @(videoControlsPosition)" style="aspect-ratio: @(videoAspectRatio);"> 380 <div id="@(playerId)" 381 class="plyr__video-embed custom" 382 data-plyr-provider="@(type)" 383 data-plyr-embed-id="@(videoId)" 384 style="--plyr-color-main: var(--swift-foreground-color);"> 385 </div> 386 </div> 387 388 <script type="module" src="/Files/Templates/Designs/Swift/Assets/js/plyr.js"></script> 389 <script type="module"> 390 var player = new Plyr('#@(playerId)', { 391 type: 'video', 392 fullscreen: { 393 enabled: true, 394 iosNative: true 395 }, 396 youtube: { 397 noCookie: true, 398 showinfo: 0 399 }, 400 vimeo: { 401 playsinline: true, 402 }, 403 clickToPlay: false, 404 autopause: false 405 }); 406 407 @if (autoPlay && openInModal == "false") 408 { 409 <text> 410 player.config.autoplay = false; 411 player.config.muted = true; 412 player.config.volume = 0; 413 414 let useIntersecting = true; 415 416 player.on('ready', function (e) { 417 const observer = new IntersectionObserver(entries => { 418 entries.forEach(entry => { 419 if (useIntersecting) { 420 if (entry.isIntersecting) { 421 e.detail.plyr.play(); 422 useIntersecting = true; 423 } 424 else { 425 e.detail.plyr.pause(); 426 useIntersecting = true; 427 } 428 } 429 }); 430 }); 431 observer.observe(e.target.parentElement); 432 }); 433 434 player.on('play', () => { 435 useIntersecting = true; 436 }); 437 438 player.on('pause', () => { 439 useIntersecting = false; 440 }); 441 442 player.on('ended', () => { 443 player.stop(); 444 player.restart(); 445 useIntersecting = false; 446 }); 447 448 </text> 449 } 450 451 @if (openInModal == "true") 452 { 453 <text> 454 var productDetailsGalleryModal = document.querySelector('#modal_@(Model.ID)') 455 productDetailsGalleryModal.addEventListener('hidden.bs.modal', function (event) { 456 player.media.pause(); 457 }) 458 </text> 459 } 460 461 initPlyrFit(player); 462 </script> 463 } 464 else 465 { 466 string autoPlayAttributes = (autoPlay && openInModal == "false") ? "loop autoplay muted playsinline" : ""; 467 string videoType = Path.GetExtension(assetValue).ToLower(); 468 string videoPathEncoded = System.Uri.EscapeDataString(assetValue); 469 470 <video preload="auto" @(autoPlayAttributes) class="h-100 w-100" style="object-fit: cover;" controls> 471 <source src="@(videoPathEncoded)#t=0.001" type="video/@videoType.Replace(".", "")"> 472 </video> 473 } 474 </div> 475 //--CUSTOM 476 } 477 } 478 } 479 } 480 @foreach (string documentFormat in supportedDocumentFormats) 481 { //Documents 482 if (assetValue.IndexOf(documentFormat, StringComparison.OrdinalIgnoreCase) >= 0) 483 { 484 if (product is object) 485 { 486 string iconPath = "/Files/Templates/Designs/Swift/Assets/icons/"; 487 488 string productName = product.Name; 489 string imagePath = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value; 490 string imageLinkPath = imagePath; 491 492 RatioSettings ratioSettings = GetRatioSettings(size); 493 494 var parms = new Dictionary<string, object>(); 495 parms.Add("alt", productName + asset.Keywords); 496 parms.Add("itemprop", "image"); 497 parms.Add("fullwidth", true); 498 parms.Add("columns", Model.GridRowColumnCount); 499 if (!string.IsNullOrEmpty(asset.DisplayName)) 500 { 501 parms.Add("title", asset.DisplayName); 502 } 503 504 if (ratioSettings.Ratio == "fill" && galleryLayout != "grid") 505 { 506 parms.Add("cssClass", "w-100 h-100 image-zoom-lg-l-hover"); 507 } 508 else 509 { 510 parms.Add("cssClass", "mw-100 mh-100"); 511 } 512 513 <a href="@imageLinkPath" class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)" download alt="@HtmlEncoder.HtmlAttributeEncode(Translate("Download"))"> @*//CUSTOM*@ 514 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100"> 515 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(iconPath + "download.svg")</div> 516 @if (asset.Value.IndexOf(".pdf", StringComparison.OrdinalIgnoreCase) >= 0) 517 { 518 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms) 519 } 520 </div> 521 </a> 522 } 523 524 } 525 } 526 </div> 527 } 528 529 530 </div> 531 assetNumber++; 532 } 533 } 534 } 535 @*//CUSTOM*@ 536 @if (totalAssets > 1 && useSwiffySlider) 537 { 538 <button class="carousel-control-prev carousel-control-area" type="button" data-bs-target="#SmallScreenImages_@Model.ID" data-bs-slide="prev"> 539 <span class="carousel-control-prev-icon" aria-hidden="true"></span> 540 <span class="visually-hidden">@Translate("Previous")</span> 541 </button> 542 <button class="carousel-control-next carousel-control-area" type="button" data-bs-target="#SmallScreenImages_@Model.ID" data-bs-slide="next"> 543 <span class="carousel-control-next-icon" aria-hidden="true"></span> 544 <span class="visually-hidden">@Translate("Next")</span> 545 </button> 546 } 547 @*//--CUSTOM*@ 548 </div> 549 @if (showBadges) 550 { 551 <div class="position-absolute top-0 left-0 p-2 p-lg-3"> 552 @{@RenderPartial("Components/EcommerceBadge.cshtml", product, badgeParms)} 553 </div> 554 } 555 556 </div> 557 558 @if (totalAssets > 1) 559 { 560 <div id="SmallScreenImagesThumbnails_@Model.ID" class="@(GetThumbnailRowSettingCss())" @(useSwiffySlider ? $"data-swiffyslider-class=\"{GetThumbnailRowSettingCss()} swiffy-slider slider-nav-square-small slider-item-reveal slider-nav-outside\" style=\"--swiffy-slider-nav-light: var(--swift-foreground-color); --swiffy-slider-nav-dark: var(--swift-background-color);\"" : null)> @*//CUSTOM*@ 561 <div class="@(GetThumbnailRowSettingCssWrapper()) gap-3" @(useSwiffySlider ? "data-swiffyslider-class=\"slider-container\"" : null)> @*//CUSTOM*@ 562 563 @foreach (MediaViewModel asset in assetsList) 564 { 565 var assetValue = asset.Value; 566 foreach (string format in allSupportedFormats) 567 { 568 if (assetValue.IndexOf(format, StringComparison.OrdinalIgnoreCase) >= 0) 569 { 570 string imagePath = Dynamicweb.Context.Current.Server.UrlEncode(assetValue); 571 imagePath = assetValue.IndexOf("youtu.be", StringComparison.OrdinalIgnoreCase) >= 0 || assetValue.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) >= 0 ? "https://img.youtube.com/vi/" + assetValue.Substring(assetValue.LastIndexOf('/') + 1) + "/mqdefault.jpg" : imagePath; 572 string imagePathThumb = assetValue.StartsWith("/Files/", StringComparison.OrdinalIgnoreCase) ? imagePath.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) < 0 && imagePath.IndexOf(".mp4", StringComparison.OrdinalIgnoreCase) < 0 ? $"/Admin/Public/GetImage.ashx?image={imagePath}&width=180&format=webp" : imagePath : assetValue; 573 string iconPath = "/Files/Templates/Designs/Swift/Assets/icons/"; 574 575 string videoId = assetValue.Substring(assetValue.LastIndexOf('/') + 1); 576 string vimeoJsClass = assetValue.IndexOf("vimeo", StringComparison.OrdinalIgnoreCase) >= 0 ? "js-vimeo-video-thumbnail" : ""; 577 578 bool isDocument = false; 579 foreach (string documentFormat in supportedDocumentFormats) 580 { 581 if (assetValue.IndexOf(documentFormat, StringComparison.OrdinalIgnoreCase) >= 0) 582 { 583 isDocument = true; 584 } 585 } 586 587 string assetName = asset.Name; 588 assetName += !string.IsNullOrEmpty(asset.Keywords) ? " " + asset.Keywords : ""; 589 string assetTitle = !string.IsNullOrEmpty(asset.DisplayName) ? "title=\"" + HtmlEncoder.HtmlAttributeEncode(asset.DisplayName) + "\"" : ""; //CUSTOM 590 if (!isDocument) 591 { 592 RatioSettings ratioSettings = GetRatioSettings("desktop"); 593 594 <div class="border outline-none ratio ratio-4x3" style="cursor: pointer; min-width: 4rem; max-width: 5rem;" data-bs-target="#SmallScreenImages_@Model.ID" data-bs-slide-to="@thumbnailNumber"> @*//CUSTOM*@ 595 <div class="d-flex align-items-center justify-content-center overflow-hidden position-absolute h-100"> 596 @foreach (string videoFormat in supportedVideoFormats) 597 { //Videos 598 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0) 599 { 600 <div class="icon-3 position-absolute text-light" style="z-index: 1">@ReadFile(iconPath + "play-circle.svg")</div> 601 } 602 } 603 </div> 604 605 @if (imagePathThumb.IndexOf(".mp4", StringComparison.OrdinalIgnoreCase) < 0) 606 { 607 <img src="@imagePathThumb" alt="@HtmlEncoder.HtmlAttributeEncode(assetName)" @assetTitle class="p-1 @vimeoJsClass w-100 h-100" style="object-fit: cover;" data-video-id="@videoId"> @*//CUSTOM*@ 608 } 609 else 610 { 611 string videoType = Path.GetExtension(asset.Value).ToLower(); 612 string videoPathEncoded = System.Uri.EscapeDataString(assetValue); 613 614 <video preload="auto" class="h-100 w-100" style="object-fit: contain;"> 615 <source src="@(videoPathEncoded)#t=0.001" type="video/@videoType.Replace(".", "")"> 616 </video> 617 } 618 </div> 619 620 } 621 else 622 { 623 <a href="@assetValue" class="ratio ratio-4x3 border outline-none" style="cursor: pointer; min-width: 4rem; max-width: 5rem;" download title="@HtmlEncoder.HtmlAttributeEncode(asset.Value)"> @*//CUSTOM*@ 624 @if (asset.Value.IndexOf(".pdf", StringComparison.OrdinalIgnoreCase) >= 0) 625 { 626 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100"> 627 <div class="icon-3 position-absolute text-light" style="z-index: 1">@ReadFile(iconPath + "download.svg")</div> 628 </div> 629 <img src="@imagePathThumb" alt="@HtmlEncoder.HtmlAttributeEncode(assetName)" @assetTitle class="p-0 p-lg-1 mw-100 mh-100" style="object-fit: cover;"> @*//CUSTOM*@ 630 } 631 else 632 { 633 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100"> 634 <div class="icon-3 position-absolute" style="z-index: 1">@ReadFile(iconPath + "file-text.svg")</div> 635 </div> 636 } 637 </a> 638 } 639 640 thumbnailNumber++; 641 } 642 } 643 } 644 </div> @*//CUSTOM*@ 645 646 @*//CUSTOM*@ 647 @if (useSwiffySlider) 648 { 649 <button type="button" title="@HtmlEncoder.HtmlAttributeEncode(Translate("Previous slide"))" class="slider-nav" style="z-index:2;"> 650 <span class="visually-hidden">@Translate("Previous slide")</span> 651 </button> 652 <button type="button" title="@HtmlEncoder.HtmlAttributeEncode(Translate("Next slide"))" class="slider-nav slider-nav-next" style="z-index:2;"> 653 <span class="visually-hidden">@Translate("Next slide")</span> 654 </button> 655 } 656 @*//--CUSTOM*@ 657 </div> 658 } 659 </div> 660 661 //CUSTOM 662 if (totalAssets > 1 && useSwiffySlider) 663 { 664 <script type="module" src="/Files/Templates/Designs/Swift/Assets/js/swiffy-slider.js"></script> 665 <script type="module"> 666 swift.AssetLoader.Load('/Files/Templates/Designs/Swift/Assets/css/swiffy-slider.min.css', 'css'); 667 document.addEventListener('load.swift.assetloader', (e) => { 668 if (e == null || e.detail == null || e.detail.parentEvent == null || e.detail.parentEvent.target == null || e.detail.parentEvent.target.href == null || !e.detail.parentEvent.target.href.includes('swiffy-slider.min.css')) return; 669 670 const sliderEl = document.querySelector('#SmallScreenImagesThumbnails_@Model.ID'); 671 const sliderWrapperEl = sliderEl.querySelector('div'); 672 673 swiffyslider.initSlider(sliderEl); 674 675 function updateSwiffySlider() { 676 if (sliderEl.hasAttribute("data-class")) { 677 sliderEl.setAttribute('class', sliderEl.getAttribute('data-class')); 678 } 679 if (sliderWrapperEl.hasAttribute("data-class")) { 680 sliderWrapperEl.setAttribute('class', sliderWrapperEl.getAttribute('data-class')); 681 } 682 if (sliderEl.scrollWidth > sliderEl.offsetWidth && window.innerWidth > 991) { 683 sliderEl.setAttribute('data-class', sliderEl.getAttribute("class")); 684 sliderEl.setAttribute('class', sliderEl.getAttribute('data-swiffyslider-class')); 685 sliderWrapperEl.setAttribute('data-class', sliderWrapperEl.getAttribute("class")); 686 sliderWrapperEl.setAttribute('class', sliderWrapperEl.getAttribute('data-swiffyslider-class')); 687 688 sliderEl.style.setProperty('--swiffy-slider-item-count', Math.min(10, Math.max(1, Math.ceil(sliderEl.offsetWidth / 120) - 1))); 689 swiffyslider.slideTo(sliderEl, 0); 690 } 691 } 692 693 updateSwiffySlider(); 694 window.addEventListener('resize', updateSwiffySlider); 695 }); 696 697 document.addEventListener("DOMContentLoaded", () => { 698 const carouselEl = document.getElementById("SmallScreenImages_@Model.ID"); 699 const carouselCounterEl = carouselEl.querySelector(".carousel-counter"); 700 701 carouselEl.addEventListener("slid.bs.carousel", function (e) { 702 carouselCounterEl.textContent = `${(e.to + 1)} / @(totalAssets)`; 703 }); 704 }); 705 </script> 706 } 707 //--CUSTOM 708 709 @* Modal with slides *@ 710 <div class="modal fade swift_products-details-images-modal" id="modal_@Model.ID" tabindex="-1" aria-labelledby="productDetailsGalleryModalTitle_@Model.ID" aria-hidden="true"> 711 <div class="modal-dialog modal-dialog-centered modal-xl"> 712 <div class="modal-content"> 713 <div class="modal-header visually-hidden"> 714 <h5 class="modal-title" id="productDetailsGalleryModalTitle_@Model.ID">@product.Title</h5> 715 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 716 </div> 717 <div class="modal-body p-2 p-lg-3 h-100"> 718 <div id="ModalCarousel_@Model.ID" class="carousel@(GetArrowsColor()) h-100" data-bs-ride="carousel"> 719 <div class="carousel-inner h-100 @theme"> 720 @foreach (MediaViewModel asset in assetsList) 721 { 722 var assetValue = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value; 723 foreach (string supportedFormat in supportedImageFormats.Concat(supportedVideoFormats).ToArray()) 724 { 725 if (assetValue.IndexOf(supportedFormat, StringComparison.OrdinalIgnoreCase) >= 0) 726 { 727 string imagePath = assetValue; 728 string activeSlide = modalAssetNumber == 0 ? "active" : ""; 729 730 var parms = new Dictionary<string, object>(); 731 parms.Add("cssClass", "d-block mw-100 mh-100 m-auto"); 732 parms.Add("fullwidth", true); 733 parms.Add("columns", Model.GridRowColumnCount); 734 735 <div class="carousel-item @activeSlide h-100" data-bs-interval="99999"> 736 @foreach (string imageFormat in supportedImageFormats) 737 { //Images 738 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0) 739 { 740 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms) 741 } 742 } 743 744 @foreach (string videoFormat in supportedVideoFormats) 745 { //Videos 746 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0) 747 { 748 if (product is object) 749 { 750 string videoPlayerSize = "modal"; 751 string assetName = !string.IsNullOrEmpty(asset.DisplayName) ? asset.DisplayName : asset.Name; 752 assetValue = asset.Value; 753 string videoId = asset.Value.Substring(asset.Value.LastIndexOf('/') + 1); 754 string type = assetValue.IndexOf("youtu.be", StringComparison.OrdinalIgnoreCase) >= 0 || assetValue.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) >= 0 ? "youtube" : ""; 755 type = assetValue.IndexOf("vimeo", StringComparison.OrdinalIgnoreCase) >= 0 ? "vimeo" : type; 756 type = assetValue.IndexOf(".mp4", StringComparison.OrdinalIgnoreCase) >= 0 || assetValue.IndexOf(".webm", StringComparison.OrdinalIgnoreCase) >= 0 ? "selfhosted" : type; 757 758 string openInModal = Model.Item.GetString("OpenVideoInModal"); 759 bool autoPlay = Model.Item.GetBoolean("VideoAutoPlay"); 760 761 <div class="h-100" itemscope itemtype="https://schema.org/VideoObject"> 762 <span class="visually-hidden" itemprop="name">@assetName</span> 763 <span class="visually-hidden" itemprop="contentUrl">@asset.Value</span> 764 <span class="visually-hidden" itemprop="thumbnailUrl">@asset.Value</span> 765 766 @if (type != "selfhosted") 767 { 768 var playerId = $"player_{Pageview.CurrentParagraph.ID}_{videoId}_{videoPlayerSize}"; 769 770 <div class="h-100 plyr__wrap plyr__wrap-aspect"> 771 <div id="@(playerId)" 772 class="plyr__video-embed" 773 data-plyr-provider="@(type)" 774 data-plyr-embed-id="@videoId" 775 style="--plyr-color-main: var(--swift-foreground-color);"> 776 </div> 777 </div> 778 779 <script type="module" src="/Files/Templates/Designs/Swift/Assets/js/plyr.js"></script> 780 <script type="module"> 781 var player = new Plyr('#@(playerId)', { 782 type: 'video', 783 fullscreen: { 784 enabled: true, 785 iosNative: true 786 }, 787 youtube: { 788 noCookie: true, 789 showinfo: 0 790 }, 791 vimeo: { 792 playsinline: true, 793 }, 794 clickToPlay: false, 795 autopause: false 796 }); 797 798 @if (autoPlay && openInModal == "false") 799 { 800 <text> 801 player.config.autoplay = true; 802 player.config.muted = true; 803 player.config.volume = 0; 804 player.media.loop = true; 805 806 player.on('ready', function() { 807 if (player.config.autoplay === true) { 808 player.media.play(); 809 } 810 }); 811 </text> 812 } 813 814 @if (openInModal == "true") 815 { 816 <text> 817 var productDetailsGalleryModal = document.querySelector('#modal_@Model.ID') 818 productDetailsGalleryModal.addEventListener('hidden.bs.modal', function (event) { 819 player.pause(); 820 }) 821 </text> 822 } 823 824 initPlyrFit(player); 825 </script> 826 } 827 else 828 { 829 string autoPlayAttributes = (autoPlay && openInModal == "false") ? "loop autoplay muted playsinline" : ""; 830 string videoType = Path.GetExtension(assetValue).ToLower(); 831 string videoPathEncoded = System.Uri.EscapeDataString(assetValue); 832 833 <video preload="auto" @autoPlayAttributes class="h-100 w-100" style="object-fit: cover;" controls> 834 <source src="@(videoPathEncoded)#t=0.001" type="video/@videoType.Replace(".", "")"> 835 </video> 836 } 837 </div> 838 } 839 } 840 } 841 </div> 842 modalAssetNumber++; 843 } 844 } 845 } 846 <button class="carousel-control-prev carousel-control-area" type="button" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide="prev"> 847 <span class="carousel-control-prev-icon" aria-hidden="true"></span> 848 <span class="visually-hidden">@Translate("Previous")</span> 849 </button> 850 <button class="carousel-control-next carousel-control-area" type="button" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide="next"> 851 <span class="carousel-control-next-icon" aria-hidden="true"></span> 852 <span class="visually-hidden">@Translate("Next")</span> 853 </button> 854 </div> 855 </div> 856 </div> 857 </div> 858 </div> 859 </div> 860 } 861 else if (Pageview.IsVisualEditorMode) 862 { 863 RatioSettings ratioSettings = GetRatioSettings("desktop"); 864 865 <div class="h-100 @theme"> 866 <div class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)"> 867 <img src="/Files/Images/missing_image.jpg" loading="lazy" decoding="async" class="mh-100 mw-100" style="object-fit: cover;"> 868 </div> 869 </div> 870 } 871 } 872 else if (Pageview.IsVisualEditorMode) 873 { 874 <div class="alert alert-dark m-0">@Translate("No products available")</div> 875 } 876
€ 0,00
OutOfStock
You will receive an email when the product is back in stock.
![]()
![]()
![]()
Delivery from €5
Free delivery on orders over €89
14 days full refund
| Height (cm) | 0 |
|---|---|
| Width (cm) | 0 |
| Depth (cm) | 0 |
| Weight (kg): | 0 |
| Designer: | 3Part A/S |
| Brand | Eva Solo |
| Series | Carry |
| SKU | 97701201011 |
| EAN | 5706631254977 |
Eva Solo community
Tag us in your instagram post to be featured in our Eva Solo community.
Use one of the following tags: @evasolo_official, #evasolo, #evatrio or #evasolofurniture