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
![]()
![]()
![]()
![]()
![]()
Fragt fra 29,- • Fri fragt ved køb for 499,-
Leveringstid: 1-3 hverdage
Mulighed for gratis retur
| Højde (cm) | 0 |
|---|---|
| Bredde (cm) | 0 |
| Dybde (cm) | 0 |
| Vægt (kg) | 0 |
| Designer | 3Part A/S |
| Brand | Eva Solo |
| Serie | Carry |
| Varenummer | 97701201011 |
| EAN | 5706631254977 |
Ved alle forsendelsestyper modtager du track-and-trace info på mail eller SMS.
GLS pakkeshop: 29 DKK | GRATIS ved køb for 499 DKK
Leveringstid: 1-3 hverdage.
Privatadresse: 29 DKK | GRATIS ved køb for 799 DKK
Leveringstid: 1-3 hverdage.
Arbejdsplads / erhvervsadresse: 29 DKK | GRATIS ved køb for 499 DKK
Leveringstid: 1-3 hverdage.
Levering med fragtmand: 29 DKK | GRATIS ved køb for 499 DKK
På relevante produkter samt i checkout kan du se om et produkt bliver sendt med fragtmand.
Du bliver kontaktet af vores kundeservice for nærmere aftale om hvilken dag levering passer dig.
Eva Solo community
Tag os i dit Instagram opslag for at blive vist i vores Eva Solo Community.
Brug et af følgende tags: @evasolo_official, #evasolo, #evatrio eller #evasolofurniture.