fix(site): stabilize media and notification runtime
This commit is contained in:
parent
12493711d1
commit
c506f242d8
|
|
@ -3,3 +3,4 @@
|
||||||
.project-video-detail .project-video-detail-copy{position:relative;z-index:2}.project-video-detail .project-video-detail-copy-bottom{margin-top:2.5rem}.project-video-detail .project-video-detail-copy-bottom .main-h{margin-bottom:0}@media screen and (max-width:767px){.project-video-detail .project-video-detail-hgroup{flex-flow:row;gap:1.25rem}.project-video-detail .project-video-detail-hgroup.flip{flex-flow:row-reverse}.project-video-detail .project-video-detail-hgroup .main-h.h1{font-size:2.35rem}.project-video-detail .project-video-detail-hgroup .h-txt{max-width:22ch}.project-video-detail .project-video-detail-copy-bottom{margin-top:2rem}}@media screen and (max-width:479px){.project-video-detail .project-video-detail-copy{width:86%}.project-video-detail .project-video-detail-hgroup{gap:1rem}.project-video-detail .project-video-detail-hgroup .main-h.h1{font-size:2rem}.project-video-detail .project-video-detail-hgroup .h-txt{max-width:17ch;font-size:.86rem;line-height:1.28em}}
|
.project-video-detail .project-video-detail-copy{position:relative;z-index:2}.project-video-detail .project-video-detail-copy-bottom{margin-top:2.5rem}.project-video-detail .project-video-detail-copy-bottom .main-h{margin-bottom:0}@media screen and (max-width:767px){.project-video-detail .project-video-detail-hgroup{flex-flow:row;gap:1.25rem}.project-video-detail .project-video-detail-hgroup.flip{flex-flow:row-reverse}.project-video-detail .project-video-detail-hgroup .main-h.h1{font-size:2.35rem}.project-video-detail .project-video-detail-hgroup .h-txt{max-width:22ch}.project-video-detail .project-video-detail-copy-bottom{margin-top:2rem}}@media screen and (max-width:479px){.project-video-detail .project-video-detail-copy{width:86%}.project-video-detail .project-video-detail-hgroup{gap:1rem}.project-video-detail .project-video-detail-hgroup .main-h.h1{font-size:2rem}.project-video-detail .project-video-detail-hgroup .h-txt{max-width:17ch;font-size:.86rem;line-height:1.28em}}
|
||||||
.project-composition .project-composition-copy{position:relative;z-index:2}.project-composition .project-composition-copy-bottom{margin-top:2.5rem}.project-composition .project-composition-copy-bottom .main-h{margin-bottom:0}@media screen and (max-width:767px){.project-composition .project-composition-hgroup{flex-flow:row;gap:1.25rem}.project-composition .project-composition-hgroup.flip{flex-flow:row-reverse}.project-composition .project-composition-hgroup .main-h.h2{font-size:2.35rem}.project-composition .project-composition-hgroup .h-txt{max-width:22ch}.project-composition .project-composition-copy-bottom{margin-top:2rem}}@media screen and (max-width:479px){.project-composition .project-composition-copy{width:86%}.project-composition .project-composition-hgroup{gap:1rem}.project-composition .project-composition-hgroup .main-h.h2{font-size:2rem}.project-composition .project-composition-hgroup .h-txt{max-width:17ch;font-size:.86rem;line-height:1.28em}}
|
.project-composition .project-composition-copy{position:relative;z-index:2}.project-composition .project-composition-copy-bottom{margin-top:2.5rem}.project-composition .project-composition-copy-bottom .main-h{margin-bottom:0}@media screen and (max-width:767px){.project-composition .project-composition-hgroup{flex-flow:row;gap:1.25rem}.project-composition .project-composition-hgroup.flip{flex-flow:row-reverse}.project-composition .project-composition-hgroup .main-h.h2{font-size:2.35rem}.project-composition .project-composition-hgroup .h-txt{max-width:22ch}.project-composition .project-composition-copy-bottom{margin-top:2rem}}@media screen and (max-width:479px){.project-composition .project-composition-copy{width:86%}.project-composition .project-composition-hgroup{gap:1rem}.project-composition .project-composition-hgroup .main-h.h2{font-size:2rem}.project-composition .project-composition-hgroup .h-txt{max-width:17ch;font-size:.86rem;line-height:1.28em}}
|
||||||
.pricing-table-section .pricing-table-shell{padding-top:12svh;padding-bottom:12svh}@media screen and (max-width:767px){.pricing-table-section .pricing-table-shell{padding-top:8svh;padding-bottom:8svh}}
|
.pricing-table-section .pricing-table-shell{padding-top:12svh;padding-bottom:12svh}@media screen and (max-width:767px){.pricing-table-section .pricing-table-shell{padding-top:8svh;padding-bottom:8svh}}
|
||||||
|
[data-module=notification-w][data-notification-stack-ready]{height:auto;max-height:calc(100svh - 3.2rem);overflow:visible;scrollbar-width:none;overscroll-behavior:contain}[data-module=notification-w][data-notification-stack-ready]::-webkit-scrollbar{display:none}[data-module=notification-w][data-notification-stack-ready]:not(:hover):not([data-notification-expanded=true]){height:var(--notification-collapsed-height,6.6rem);max-height:var(--notification-collapsed-height,6.6rem)}[data-module=notification-w][data-notification-stack-ready]:is(:hover,[data-notification-expanded=true]){overflow-x:visible;overflow-y:auto;padding-bottom:.2rem}[data-module=notification-w][data-notification-stack-ready]>[data-module=notification]{position:relative;z-index:var(--notification-z,1);transform:translateY(var(--notification-stack-y,0));transition:transform .3s cubic-bezier(.22,1,.36,1)}[data-module=notification-w][data-notification-stack-ready]:is(:hover,[data-notification-expanded=true])>[data-module=notification]{transform:translateY(0)}
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@
|
||||||
.project-video-detail .project-video-detail-copy{position:relative;z-index:2}.project-video-detail .project-video-detail-copy-bottom{margin-top:2.5rem}.project-video-detail .project-video-detail-copy-bottom .main-h{margin-bottom:0}@media screen and (max-width:767px){.project-video-detail .project-video-detail-hgroup{flex-flow:row;gap:1.25rem}.project-video-detail .project-video-detail-hgroup.flip{flex-flow:row-reverse}.project-video-detail .project-video-detail-hgroup .main-h.h1{font-size:2.35rem}.project-video-detail .project-video-detail-hgroup .h-txt{max-width:22ch}.project-video-detail .project-video-detail-copy-bottom{margin-top:2rem}}@media screen and (max-width:479px){.project-video-detail .project-video-detail-copy{width:86%}.project-video-detail .project-video-detail-hgroup{gap:1rem}.project-video-detail .project-video-detail-hgroup .main-h.h1{font-size:2rem}.project-video-detail .project-video-detail-hgroup .h-txt{max-width:17ch;font-size:.86rem;line-height:1.28em}}
|
.project-video-detail .project-video-detail-copy{position:relative;z-index:2}.project-video-detail .project-video-detail-copy-bottom{margin-top:2.5rem}.project-video-detail .project-video-detail-copy-bottom .main-h{margin-bottom:0}@media screen and (max-width:767px){.project-video-detail .project-video-detail-hgroup{flex-flow:row;gap:1.25rem}.project-video-detail .project-video-detail-hgroup.flip{flex-flow:row-reverse}.project-video-detail .project-video-detail-hgroup .main-h.h1{font-size:2.35rem}.project-video-detail .project-video-detail-hgroup .h-txt{max-width:22ch}.project-video-detail .project-video-detail-copy-bottom{margin-top:2rem}}@media screen and (max-width:479px){.project-video-detail .project-video-detail-copy{width:86%}.project-video-detail .project-video-detail-hgroup{gap:1rem}.project-video-detail .project-video-detail-hgroup .main-h.h1{font-size:2rem}.project-video-detail .project-video-detail-hgroup .h-txt{max-width:17ch;font-size:.86rem;line-height:1.28em}}
|
||||||
.project-composition .project-composition-copy{position:relative;z-index:2}.project-composition .project-composition-copy-bottom{margin-top:2.5rem}.project-composition .project-composition-copy-bottom .main-h{margin-bottom:0}@media screen and (max-width:767px){.project-composition .project-composition-hgroup{flex-flow:row;gap:1.25rem}.project-composition .project-composition-hgroup.flip{flex-flow:row-reverse}.project-composition .project-composition-hgroup .main-h.h2{font-size:2.35rem}.project-composition .project-composition-hgroup .h-txt{max-width:22ch}.project-composition .project-composition-copy-bottom{margin-top:2rem}}@media screen and (max-width:479px){.project-composition .project-composition-copy{width:86%}.project-composition .project-composition-hgroup{gap:1rem}.project-composition .project-composition-hgroup .main-h.h2{font-size:2rem}.project-composition .project-composition-hgroup .h-txt{max-width:17ch;font-size:.86rem;line-height:1.28em}}
|
.project-composition .project-composition-copy{position:relative;z-index:2}.project-composition .project-composition-copy-bottom{margin-top:2.5rem}.project-composition .project-composition-copy-bottom .main-h{margin-bottom:0}@media screen and (max-width:767px){.project-composition .project-composition-hgroup{flex-flow:row;gap:1.25rem}.project-composition .project-composition-hgroup.flip{flex-flow:row-reverse}.project-composition .project-composition-hgroup .main-h.h2{font-size:2.35rem}.project-composition .project-composition-hgroup .h-txt{max-width:22ch}.project-composition .project-composition-copy-bottom{margin-top:2rem}}@media screen and (max-width:479px){.project-composition .project-composition-copy{width:86%}.project-composition .project-composition-hgroup{gap:1rem}.project-composition .project-composition-hgroup .main-h.h2{font-size:2rem}.project-composition .project-composition-hgroup .h-txt{max-width:17ch;font-size:.86rem;line-height:1.28em}}
|
||||||
.pricing-table-section .pricing-table-shell{padding-top:12svh;padding-bottom:12svh}@media screen and (max-width:767px){.pricing-table-section .pricing-table-shell{padding-top:8svh;padding-bottom:8svh}}
|
.pricing-table-section .pricing-table-shell{padding-top:12svh;padding-bottom:12svh}@media screen and (max-width:767px){.pricing-table-section .pricing-table-shell{padding-top:8svh;padding-bottom:8svh}}
|
||||||
|
[data-module=notification-w][data-notification-stack-ready]{height:auto;max-height:calc(100svh - 3.2rem);overflow:visible;scrollbar-width:none;overscroll-behavior:contain}[data-module=notification-w][data-notification-stack-ready]::-webkit-scrollbar{display:none}[data-module=notification-w][data-notification-stack-ready]:not(:hover):not([data-notification-expanded=true]){height:var(--notification-collapsed-height,6.6rem);max-height:var(--notification-collapsed-height,6.6rem)}[data-module=notification-w][data-notification-stack-ready]:is(:hover,[data-notification-expanded=true]){overflow-x:visible;overflow-y:auto;padding-bottom:.2rem}[data-module=notification-w][data-notification-stack-ready]>[data-module=notification]{position:relative;z-index:var(--notification-z,1);transform:translateY(var(--notification-stack-y,0));transition:transform .3s cubic-bezier(.22,1,.36,1)}[data-module=notification-w][data-notification-stack-ready]:is(:hover,[data-notification-expanded=true])>[data-module=notification]{transform:translateY(0)}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@
|
||||||
const MEDIA_HANDLE_BOUND_FLAG = "__nodedcMediaHandleBound";
|
const MEDIA_HANDLE_BOUND_FLAG = "__nodedcMediaHandleBound";
|
||||||
const DOCK_LINK_BOUND_FLAG = "__nodedcDockLinkBound";
|
const DOCK_LINK_BOUND_FLAG = "__nodedcDockLinkBound";
|
||||||
const ORDERED_SLIDER_BOUND_FLAG = "__nodedcOrderedSliderBound";
|
const ORDERED_SLIDER_BOUND_FLAG = "__nodedcOrderedSliderBound";
|
||||||
|
const LAZY_MEDIA_VIDEO_BOUND_FLAG = "__nodedcLazyMediaVideoBound";
|
||||||
const SCROLL_HEIGHT_SYNC_FLAG = "__nodedcScrollHeightSyncBound";
|
const SCROLL_HEIGHT_SYNC_FLAG = "__nodedcScrollHeightSyncBound";
|
||||||
|
const NOTIFICATION_STACK_BOUND_FLAG = "__nodedcNotificationStackBound";
|
||||||
const SCROLL_HEIGHT_SYNC_WARMUP_MS = 14000;
|
const SCROLL_HEIGHT_SYNC_WARMUP_MS = 14000;
|
||||||
const SCROLL_HEIGHT_SYNC_INTERVAL_MS = 90;
|
const SCROLL_HEIGHT_SYNC_INTERVAL_MS = 90;
|
||||||
const IMAGE_EXTENSIONS = new Set(["avif", "gif", "jpeg", "jpg", "png", "svg", "webp"]);
|
const IMAGE_EXTENSIONS = new Set(["avif", "gif", "jpeg", "jpg", "png", "svg", "webp"]);
|
||||||
|
|
@ -384,6 +386,75 @@
|
||||||
document.querySelectorAll('[data-module="dock"] a[data-app]').forEach(bindDockLink);
|
document.querySelectorAll('[data-module="dock"] a[data-app]').forEach(bindDockLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshNotificationStack(wrapper) {
|
||||||
|
const notifications = Array.from(wrapper.querySelectorAll(':scope > [data-module="notification"]'));
|
||||||
|
const firstOffset = notifications[0]?.offsetTop || 0;
|
||||||
|
const collapsedHeight = (notifications[0]?.offsetHeight || 0) + Math.min(Math.max(notifications.length - 1, 0), 3) * 5;
|
||||||
|
|
||||||
|
wrapper.dataset.notificationStackReady = "true";
|
||||||
|
wrapper.style.setProperty("--notification-count", String(notifications.length));
|
||||||
|
wrapper.style.setProperty("--notification-collapsed-height", `${collapsedHeight}px`);
|
||||||
|
|
||||||
|
notifications.forEach((notification, index) => {
|
||||||
|
const naturalOffset = notification.offsetTop - firstOffset;
|
||||||
|
const deckOffset = Math.min(index, 3) * 5;
|
||||||
|
|
||||||
|
notification.style.setProperty("--notification-stack-y", `${deckOffset - naturalOffset}px`);
|
||||||
|
notification.style.setProperty("--notification-z", String(index + 1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleNotificationStackRefresh(wrapper) {
|
||||||
|
if (wrapper.__nodedcNotificationStackFrame) return;
|
||||||
|
|
||||||
|
wrapper.__nodedcNotificationStackFrame = window.requestAnimationFrame(() => {
|
||||||
|
wrapper.__nodedcNotificationStackFrame = 0;
|
||||||
|
refreshNotificationStack(wrapper);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindNotificationStack(wrapper) {
|
||||||
|
if (!wrapper || wrapper[NOTIFICATION_STACK_BOUND_FLAG]) return;
|
||||||
|
|
||||||
|
wrapper[NOTIFICATION_STACK_BOUND_FLAG] = true;
|
||||||
|
scheduleNotificationStackRefresh(wrapper);
|
||||||
|
|
||||||
|
const mutationObserver =
|
||||||
|
typeof MutationObserver === "function"
|
||||||
|
? new MutationObserver(() => scheduleNotificationStackRefresh(wrapper))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
mutationObserver?.observe(wrapper, { childList: true });
|
||||||
|
|
||||||
|
wrapper.addEventListener("pointerdown", (event) => {
|
||||||
|
if (event.target?.closest?.("[data-close]")) return;
|
||||||
|
wrapper.dataset.notificationExpanded = "true";
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.addEventListener("mouseleave", () => {
|
||||||
|
delete wrapper.dataset.notificationExpanded;
|
||||||
|
wrapper.scrollTop = 0;
|
||||||
|
scheduleNotificationStackRefresh(wrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
"pointerdown",
|
||||||
|
(event) => {
|
||||||
|
if (wrapper.contains(event.target)) return;
|
||||||
|
delete wrapper.dataset.notificationExpanded;
|
||||||
|
wrapper.scrollTop = 0;
|
||||||
|
},
|
||||||
|
{ capture: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => scheduleNotificationStackRefresh(wrapper));
|
||||||
|
window.setTimeout(() => scheduleNotificationStackRefresh(wrapper), 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindNotificationStacks() {
|
||||||
|
document.querySelectorAll('[data-module="notification-w"]').forEach(bindNotificationStack);
|
||||||
|
}
|
||||||
|
|
||||||
function mediaExtension(src) {
|
function mediaExtension(src) {
|
||||||
const cleanSrc = String(src || "").split(/[?#]/)[0];
|
const cleanSrc = String(src || "").split(/[?#]/)[0];
|
||||||
const match = /\.([a-z0-9]+)$/i.exec(cleanSrc);
|
const match = /\.([a-z0-9]+)$/i.exec(cleanSrc);
|
||||||
|
|
@ -407,7 +478,7 @@
|
||||||
|
|
||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
video.className = "video-el";
|
video.className = "video-el";
|
||||||
video.dataset.module = "video-handle";
|
video.dataset.lazyVideo = "";
|
||||||
video.src = src;
|
video.src = src;
|
||||||
video.muted = true;
|
video.muted = true;
|
||||||
video.loop = true;
|
video.loop = true;
|
||||||
|
|
@ -458,8 +529,53 @@
|
||||||
document.querySelectorAll("[data-adaptive-video] video").forEach(bindAdaptiveVideo);
|
document.querySelectorAll("[data-adaptive-video] video").forEach(bindAdaptiveVideo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playLazyVideo(video) {
|
||||||
|
video.preload = "auto";
|
||||||
|
const playPromise = video.play();
|
||||||
|
|
||||||
|
if (playPromise && typeof playPromise.catch === "function") {
|
||||||
|
playPromise.catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindLazyMediaVideo(video) {
|
||||||
|
if (!video || video[LAZY_MEDIA_VIDEO_BOUND_FLAG]) return;
|
||||||
|
|
||||||
|
video[LAZY_MEDIA_VIDEO_BOUND_FLAG] = true;
|
||||||
|
video.muted = true;
|
||||||
|
video.loop = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.preload = "none";
|
||||||
|
video.removeAttribute("autoplay");
|
||||||
|
|
||||||
|
if (typeof IntersectionObserver !== "function") {
|
||||||
|
playLazyVideo(video);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
playLazyVideo(video);
|
||||||
|
} else {
|
||||||
|
video.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ rootMargin: "80px 0px", threshold: 0.01 },
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(video);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindLazyMediaVideos() {
|
||||||
|
document.querySelectorAll("video[data-lazy-video]").forEach(bindLazyMediaVideo);
|
||||||
|
}
|
||||||
|
|
||||||
bindScrollHeightSync();
|
bindScrollHeightSync();
|
||||||
hydrateMediaHandles();
|
hydrateMediaHandles();
|
||||||
|
bindNotificationStacks();
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
|
|
@ -470,7 +586,9 @@
|
||||||
bindInteractiveScroll();
|
bindInteractiveScroll();
|
||||||
bindOrderedSliders();
|
bindOrderedSliders();
|
||||||
bindDockLinks();
|
bindDockLinks();
|
||||||
|
bindNotificationStacks();
|
||||||
bindAdaptiveVideos();
|
bindAdaptiveVideos();
|
||||||
|
bindLazyMediaVideos();
|
||||||
scheduleScrollHeightSync(true);
|
scheduleScrollHeightSync(true);
|
||||||
},
|
},
|
||||||
{ once: true },
|
{ once: true },
|
||||||
|
|
@ -481,7 +599,9 @@
|
||||||
bindInteractiveScroll();
|
bindInteractiveScroll();
|
||||||
bindOrderedSliders();
|
bindOrderedSliders();
|
||||||
bindDockLinks();
|
bindDockLinks();
|
||||||
|
bindNotificationStacks();
|
||||||
bindAdaptiveVideos();
|
bindAdaptiveVideos();
|
||||||
|
bindLazyMediaVideos();
|
||||||
scheduleScrollHeightSync(true);
|
scheduleScrollHeightSync(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -491,14 +611,18 @@
|
||||||
bindInteractiveScroll();
|
bindInteractiveScroll();
|
||||||
bindOrderedSliders();
|
bindOrderedSliders();
|
||||||
bindDockLinks();
|
bindDockLinks();
|
||||||
|
bindNotificationStacks();
|
||||||
bindAdaptiveVideos();
|
bindAdaptiveVideos();
|
||||||
|
bindLazyMediaVideos();
|
||||||
scheduleScrollHeightSync(true);
|
scheduleScrollHeightSync(true);
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
hydrateMediaHandles();
|
hydrateMediaHandles();
|
||||||
bindInteractiveScroll();
|
bindInteractiveScroll();
|
||||||
bindOrderedSliders();
|
bindOrderedSliders();
|
||||||
bindDockLinks();
|
bindDockLinks();
|
||||||
|
bindNotificationStacks();
|
||||||
bindAdaptiveVideos();
|
bindAdaptiveVideos();
|
||||||
|
bindLazyMediaVideos();
|
||||||
scheduleScrollHeightSync(true);
|
scheduleScrollHeightSync(true);
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<div role="listitem" class="slide-w w-dyn-item">
|
<div role="listitem" class="slide-w w-dyn-item">
|
||||||
<div class="video-cut">
|
<div class="video-cut">
|
||||||
<div class="video-screen">
|
<div class="video-screen">
|
||||||
<video src="{{attr:videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video>
|
<video src="{{attr:videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-lazy-video="" class="video-el"></video>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hgroup class="slide-he-w">
|
<hgroup class="slide-he-w">
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
<section id="features" class="s h-screen py mobile-flip"><div class="app-w align-right"><div id="demo-video" data-module="app" class="app-item video" data-adaptive-video-window style="--nodedc-video-height: {{attr:content.videoHeightPx}}px;"><div data-draggable-area="" class="app-head preview"><div class="app-head-trigs"><button data-close="" aria-label="закрыть" class="circle opaque"><div class="x-icon"><div class="line full x-icon rot"></div><div class="line full x-icon"></div></div></button><div class="circle opaque"></div></div><div class="app-name-w"><div class="app-name-txt preview">{{text:content.windowTitle}}</div></div></div><div class="video-size" data-adaptive-video><video src="{{attr:content.videoSrc}}" autoplay="true" muted="" loop="" playsinline="true" class="video"></video></div></div></div><div class="c"><hgroup data-module="hg"><h2 class="main-h h1"><span class="darker-40">{{text:content.heading.muted}}</span><span>{{text:content.heading.main}}</span></h2><div class="h-txt">{{html:content.introHtml}}</div><div class="txt-w"><div class="hg-par darker-40">{{text:content.features.0.eyebrow}}</div><div class="h-txt">{{html:content.features.0.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.1.eyebrow}}</div><div class="h-txt">{{html:content.features.1.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.2.eyebrow}}</div><div class="h-txt">{{html:content.features.2.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.3.eyebrow}}</div><div class="h-txt">{{html:content.features.3.bodyHtml}}</div></div></hgroup></div></section>
|
<section id="features" class="s h-screen py mobile-flip"><div class="app-w align-right"><div id="demo-video" data-module="app" class="app-item video" data-adaptive-video-window style="--nodedc-video-height: {{attr:content.videoHeightPx}}px;"><div data-draggable-area="" class="app-head preview"><div class="app-head-trigs"><button data-close="" aria-label="закрыть" class="circle opaque"><div class="x-icon"><div class="line full x-icon rot"></div><div class="line full x-icon"></div></div></button><div class="circle opaque"></div></div><div class="app-name-w"><div class="app-name-txt preview">{{text:content.windowTitle}}</div></div></div><div class="video-size" data-adaptive-video><video src="{{attr:content.videoSrc}}" muted="" loop="" playsinline="true" preload="none" data-lazy-video="" class="video"></video></div></div></div><div class="c"><hgroup data-module="hg"><h2 class="main-h h1"><span class="darker-40">{{text:content.heading.muted}}</span><span>{{text:content.heading.main}}</span></h2><div class="h-txt">{{html:content.introHtml}}</div><div class="txt-w"><div class="hg-par darker-40">{{text:content.features.0.eyebrow}}</div><div class="h-txt">{{html:content.features.0.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.1.eyebrow}}</div><div class="h-txt">{{html:content.features.1.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.2.eyebrow}}</div><div class="h-txt">{{html:content.features.2.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.3.eyebrow}}</div><div class="h-txt">{{html:content.features.3.bodyHtml}}</div></div></hgroup></div></section>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
|
import { createReadStream } from "node:fs";
|
||||||
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||||
import { extname, join, normalize } from "node:path";
|
import { extname, join, normalize } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
@ -44,6 +45,30 @@ function send(res, status, body, contentType = "text/plain; charset=utf-8") {
|
||||||
res.end(body);
|
res.end(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendStream(req, res, status, stream, headers) {
|
||||||
|
res.writeHead(status, {
|
||||||
|
"cache-control": "no-store",
|
||||||
|
"accept-ranges": "bytes",
|
||||||
|
...headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (req.method === "HEAD") {
|
||||||
|
stream.destroy();
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.on("error", () => {
|
||||||
|
if (!res.headersSent) {
|
||||||
|
send(res, 500, "Failed to read file");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.destroy();
|
||||||
|
});
|
||||||
|
stream.pipe(res);
|
||||||
|
}
|
||||||
|
|
||||||
function sendJson(res, status, payload) {
|
function sendJson(res, status, payload) {
|
||||||
send(res, status, `${JSON.stringify(payload, null, 2)}\n`, "application/json; charset=utf-8");
|
send(res, status, `${JSON.stringify(payload, null, 2)}\n`, "application/json; charset=utf-8");
|
||||||
}
|
}
|
||||||
|
|
@ -314,7 +339,52 @@ async function serveStatic(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = extname(filePath);
|
const ext = extname(filePath);
|
||||||
send(res, 200, await readFile(filePath), MIME[ext] || "application/octet-stream");
|
const contentType = MIME[ext] || "application/octet-stream";
|
||||||
|
const range = req.headers.range;
|
||||||
|
|
||||||
|
if (range) {
|
||||||
|
const match = /^bytes=(\d*)-(\d*)$/.exec(range);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
res.writeHead(416, {
|
||||||
|
"cache-control": "no-store",
|
||||||
|
"content-range": `bytes */${fileStat.size}`,
|
||||||
|
});
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = match[1] ? Number(match[1]) : 0;
|
||||||
|
const end = match[2] ? Number(match[2]) : fileStat.size - 1;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isInteger(start) ||
|
||||||
|
!Number.isInteger(end) ||
|
||||||
|
start < 0 ||
|
||||||
|
end < start ||
|
||||||
|
start >= fileStat.size
|
||||||
|
) {
|
||||||
|
res.writeHead(416, {
|
||||||
|
"cache-control": "no-store",
|
||||||
|
"content-range": `bytes */${fileStat.size}`,
|
||||||
|
});
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cappedEnd = Math.min(end, fileStat.size - 1);
|
||||||
|
sendStream(req, res, 206, createReadStream(filePath, { start, end: cappedEnd }), {
|
||||||
|
"content-type": contentType,
|
||||||
|
"content-length": cappedEnd - start + 1,
|
||||||
|
"content-range": `bytes ${start}-${cappedEnd}/${fileStat.size}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendStream(req, res, 200, createReadStream(filePath), {
|
||||||
|
"content-type": contentType,
|
||||||
|
"content-length": fileStat.size,
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
send(res, 404, "Not found");
|
send(res, 404, "Not found");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue