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

0,00 kr OutOfStock
You will receive an email when the product is back in stock.

Höjd (cm) 0
Bredd (cm) 0
Djup (cm) 0
Vikt (kg) 0
Designer: 3Part A/S
Brand Eva Solo
Series Carry
Artikelnummer 97701201011
EAN 5706631254977

UPS Paketshop
59 SEK | GRATIS vid köp for 599 SEK

Privatadress
75 SEK | GRATIS ved køb for 599 SEK

Företagsadress
59 SEK | GRATIS ved køb for 599 SEK

För alla typer av e-post du får track-and-trace information per post eller SMS.

LEVERINGSTID: 1-3 arbetsdagar

Eva Solo community

Tagga oss i ditt instagram-inlägg för att synas i vår Eva Solo Community.
Använd en av följande taggar: @evasolo_official, #evasolo, #evatrio eller #evasolofurniture.

Relaterat innehåll