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
![]()
![]()
![]()
![]()
| Höhe (cm) | 0 |
|---|---|
| Breite (cm) | 0 |
| Tiefe (cm) | 0 |
| Gewicht (kg) | 0 |
| Designer: | 3Part A/S |
| Brand | Eva Solo |
| Series | Carry |
| Artikelnummer | 97701201011 |
| EAN | 5706631254977 |
Sie erhalten Informationen über die Sendung per E-Mail und / oder SMS.
Sie wählen Ihre Bestellung so, wie es Ihnen am besten passt: entweder an einen UPS Access Point, an Ihre Privatadresse oder an Ihren Arbeitsplatz. Lieferpreise variieren je nach Art der Lieferung, die Sie wählen.
Sehen Sie sich die aktuellen Versandpreise und freie Verschiffen Grenzen unten.
Versandkosten nach Deutschland, Österreich und der Schweiz:
UPS Access Point:
5 € / CHF | Kostenfrei ab 79 € / CHF
Private Adresse:
5 € / CHF | Kostenfrei ab 79 € / CHF
Geschäftsadresse:
5 € / CHF | Kostenfrei ab 79 € / CHF
Möbel-Bestellungen, auf Palette zu Bordsteinkante:
90 € / CHF
Eva Solo community
Tag Sie uns in Ihrem Instagram-Beitrag, um in unserer Eva Solo Community vorgestellt zu werden.
Verwenden Sie einen der folgenden Tags: @evasolo_official, #evasolo, #evatrio oder #evasolofurniture.