Files

14 KiB
Raw Permalink Blame History

layout, hero, features
layout hero features
home
name text tagline image actions
Lap Local-first, AI-powered Photo manager. Built for family albums, photographers, and large local libraries.
src alt
/screenshots/lap_library.png Lap local photo library manager screenshot
theme text link
brand Download https://github.com/julyx10/lap/releases/latest
theme text link
alt What's New in v0.3.2 /guide/release-notes/v0.3.2
theme text link
alt View on GitHub https://github.com/julyx10/lap
title details icon
No Cloud Required Keep your photos on your own disk. Lap is a local-first photo manager with no forced cloud upload. ☁️
title details icon
Private by Default Browsing, indexing, and search run locally on your device so your family albums stay under your control. 🔒
title details icon
Folder-First Workflow Use existing folders directly, or import photos into date-based folders with original filenames and duplicate skipping. 📂
title details icon
Local AI Search Find photos with text search, similar-image search, face clustering, and smart tags processed on-device. 🧠
title details icon
Built for Large Libraries Smooth browsing and organization across thousands of photos and videos in real-world collections. ⚡
title details icon
Free to Use No subscription plan or recurring fee. Install and manage your memories freely. 💸
title details icon
Interactive Map View Explore geotagged photos and videos on a map, grouped by location and filtered with your current view. 🗺️
title details icon
Live and Motion Photos Play Apple Live Photos and supported Google Motion Photos alongside your photos and videos. ▶️
title details icon
Review and Organize Compare photos, organize collections and tags, and clean up duplicate sets in bulk. 🖼️
<script setup> import { ref, onMounted, onBeforeUnmount } from 'vue' const isZoomed = ref(false) const imgSrc = ref('') let heroCleanup = null onMounted(() => { // Use a MutationObserver or a timeout to wait for VPHero to render if needed, // but usually onMounted is enough for static SSR content in VitePress. const interval = setInterval(() => { const heroImg = document.querySelector('.VPHero .image-src') const heroImageWrap = document.querySelector('.VPHero .image') if (heroImg && heroImageWrap) { heroImg.style.cursor = 'zoom-in' heroImg.addEventListener('click', (e) => { imgSrc.value = e.target.src isZoomed.value = true }) const handleMove = (event) => { const rect = heroImageWrap.getBoundingClientRect() const rx = ((event.clientX - rect.left) / rect.width - 0.5) * 2 const ry = ((event.clientY - rect.top) / rect.height - 0.5) * 2 heroImageWrap.style.setProperty('--hero-tilt-x', `${rx.toFixed(3)}`) heroImageWrap.style.setProperty('--hero-tilt-y', `${ry.toFixed(3)}`) } const resetMove = () => { heroImageWrap.style.setProperty('--hero-tilt-x', '0') heroImageWrap.style.setProperty('--hero-tilt-y', '0') } heroImageWrap.addEventListener('mousemove', handleMove) heroImageWrap.addEventListener('mouseleave', resetMove) heroCleanup = () => { heroImageWrap.removeEventListener('mousemove', handleMove) heroImageWrap.removeEventListener('mouseleave', resetMove) } clearInterval(interval) } }, 100) // Clean up interval after 5 seconds just in case setTimeout(() => clearInterval(interval), 5000) }) onBeforeUnmount(() => { heroCleanup?.() }) </script> <style> :root { --vp-home-hero-name-color: var(--vp-c-text-1); --vp-home-hero-name-background: none; } .VPNavBarTitle .logo { border-radius: 8px; } .VPHome { background: radial-gradient(circle at 15% 20%, rgba(14, 165, 233, 0.18), transparent 26%), radial-gradient(circle at 85% 18%, rgba(245, 158, 11, 0.16), transparent 24%), radial-gradient(circle at 72% 72%, rgba(15, 118, 110, 0.18), transparent 30%), linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 25%); } .VPHero { position: relative; overflow: hidden; } .VPHero::before, .VPHero::after { content: ""; position: absolute; border-radius: 999px; filter: blur(70px); pointer-events: none; opacity: 0.7; } .VPHero::before { top: 2rem; right: -4rem; width: 18rem; height: 18rem; background: rgba(14, 165, 233, 0.2); animation: heroFloat 12s ease-in-out infinite; } .VPHero::after { left: -3rem; bottom: 1rem; width: 16rem; height: 16rem; background: rgba(245, 158, 11, 0.16); animation: heroFloat 14s ease-in-out infinite reverse; } .VPHero .container { position: relative; z-index: 1; } .VPHero .main { position: relative; } .VPHero .name, .VPHero .text, .VPHero .tagline { max-width: 34rem; } .VPHero .name { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; } .VPHero .name::before { content: ""; width: 4.6rem; height: 4.6rem; flex: 0 0 4.6rem; border-radius: 1rem; background: url('/icon.png') center/cover no-repeat; box-shadow: 0 10px 30px rgba(14, 165, 233, 0.22), 0 4px 12px rgba(0, 0, 0, 0.16); } .VPHero .text { font-weight: 700; letter-spacing: -0.03em; } .VPHero .tagline { font-size: 1.05rem; line-height: 1.75; } .VPHero .actions { gap: 0.9rem; } .VPHero .action .VPButton { display: inline-flex; align-items: center; justify-content: center; border-radius: 999px; padding: 0 1.1rem; min-height: 2.9rem; line-height: 1; transition: transform 0.2s ease, box-shadow 0.25s ease, border-color 0.25s ease; } .VPHero .action .VPButton.brand { box-shadow: 0 16px 40px rgba(14, 165, 233, 0.28); } .VPHero .action .VPButton.alt { border-color: rgba(255, 255, 255, 0.14); background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(10px); } .VPHero .action .VPButton:hover { transform: translateY(-2px); } .VPHero .image { position: relative; --hero-tilt-x: 0; --hero-tilt-y: 0; } .VPHero .image::before { content: ""; position: absolute; inset: 10% 0% -8% 8%; border-radius: 2rem; background: linear-gradient(135deg, rgba(14, 165, 233, 0.18), rgba(15, 118, 110, 0.1)), rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 40px 120px rgba(0, 0, 0, 0.28); transform: rotate(-6deg); } .VPHero .image::after { content: ""; position: absolute; inset: 16% 8% -12% 16%; border-radius: 2rem; background: radial-gradient(circle at top, rgba(245, 158, 11, 0.24), transparent 55%), linear-gradient(115deg, transparent 28%, rgba(255, 255, 255, 0.12) 42%, transparent 56%); background-size: auto, 220% 100%; background-position: center, 140% 0; filter: blur(36px); opacity: 0.75; animation: glowSweep 8s ease-in-out infinite; } .VPHero .image-container { isolation: isolate; } .VPFeatures { position: relative; padding-top: 2rem !important; } .VPFeatures .container { max-width: 1180px !important; } .VPFeatures .items { gap: 0.75rem !important; } .VPFeatures .item { padding: 0 !important; width: 100% !important; max-width: none !important; flex: 1 1 auto !important; } .VPFeatures .VPLink { height: 100%; } .VPFeatures .box { height: 100%; border-radius: 24px !important; padding: 1.15rem 1.05rem 1.05rem !important; border: 1px solid rgba(255, 255, 255, 0.08); background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03)), rgba(255, 255, 255, 0.02); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 18px 50px rgba(0, 0, 0, 0.16); transition: transform 0.22s ease, border-color 0.22s ease, box-shadow 0.22s ease, background 0.22s ease; } .VPFeatures .box:hover { transform: translateY(-6px); border-color: rgba(14, 165, 233, 0.28); background: linear-gradient(180deg, rgba(14, 165, 233, 0.08), rgba(255, 255, 255, 0.04)), rgba(255, 255, 255, 0.03); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 24px 60px rgba(0, 0, 0, 0.22); } .VPFeatures .icon { display: inline-flex; align-items: center; justify-content: center; width: 2.8rem; height: 2.8rem; margin-bottom: 0.9rem; border-radius: 0.95rem; font-size: 1.25rem; background: linear-gradient(135deg, rgba(14, 165, 233, 0.16), rgba(245, 158, 11, 0.14)); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12); } .VPFeatures .title { font-size: 1.02rem !important; font-weight: 700 !important; line-height: 1.35 !important; } .VPFeatures .details { margin-top: 0.55rem !important; font-size: 0.95rem !important; line-height: 1.65 !important; color: var(--vp-c-text-2) !important; } @media (min-width: 960px) { .VPFeatures .items { display: grid !important; grid-template-columns: repeat(3, minmax(0, 1fr)) !important; gap: 0.85rem !important; } .VPFeatures .item { min-width: 0 !important; } .VPHero .container { display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding: 0 16px; } .VPHero.has-image .main { flex: 0 0 50% !important; width: 50% !important; max-width: none !important; padding-right: 48px; } .VPHero .image { flex: 0 0 50% !important; width: 50% !important; display: flex !important; justify-content: center; align-items: center; order: 2 !important; margin: 0 !important; } .VPHero .image-container { width: 100% !important; height: auto !important; max-width: none !important; transform: none !important; perspective: 1600px !important; position: relative !important; } .VPHero .image-src, .VPHero .image-bg { position: relative !important; top: auto !important; left: auto !important; transform: none !important; max-width: 100% !important; max-height: none !important; width: 100% !important; } .VPHero .image-container::before, .VPHero .image-container::after { content: ""; position: absolute; inset: auto; width: 78%; aspect-ratio: 16 / 10; border-radius: 18px; background-size: cover; background-position: center; border: 1px solid rgba(255, 255, 255, 0.12); box-shadow: 0 28px 80px rgba(0, 0, 0, 0.26); transform-style: preserve-3d; pointer-events: none; } .VPHero .image-container::before { top: 9%; left: -7%; background-image: url('/screenshots/lap_map_view.png'); transform: rotateX(calc(10deg + var(--hero-tilt-y) * -2deg)) rotateY(calc(22deg + var(--hero-tilt-x) * 4deg)) rotateZ(-10deg) translateY(calc(var(--hero-tilt-y) * 10px)) scale(0.9); opacity: 0.92; animation: sideCardLeft 10s ease-in-out infinite; } .VPHero .image-container::after { right: -5%; bottom: 6%; background-image: url('/screenshots/lap-home3.png'); transform: rotateX(calc(2deg + var(--hero-tilt-y) * 2deg)) rotateY(calc(-18deg + var(--hero-tilt-x) * 4deg)) rotateZ(8deg) translateY(calc(var(--hero-tilt-y) * -8px)) scale(0.86); opacity: 0.88; animation: sideCardRight 11s ease-in-out infinite; } .VPHero .image-src { border-radius: 18px !important; border: 1px solid rgba(255, 255, 255, 0.16) !important; box-shadow: 0 30px 80px rgba(0, 0, 0, 0.34) !important; transform: rotateX(calc(7deg + var(--hero-tilt-y) * -3deg)) rotateY(calc(-10deg + var(--hero-tilt-x) * 7deg)) rotateZ(1deg) translate3d(calc(var(--hero-tilt-x) * 6px), calc(var(--hero-tilt-y) * -10px), 0) !important; transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.5s ease !important; position: relative !important; z-index: 2; will-change: transform; } .VPHero .image-src:hover { box-shadow: 0 40px 110px rgba(0, 0, 0, 0.42) !important; } .VPHero .name { font-size: 3.8rem !important; line-height: 1 !important; } .VPHero .text { font-size: 2.35rem !important; line-height: 1.08 !important; } } @media (max-width: 959px) { .VPHero .text { font-size: 2.1rem !important; line-height: 1.12 !important; } .VPHero .tagline { font-size: 0.98rem; } .VPHero .name::before { width: 3.6rem; height: 3.6rem; flex-basis: 3.6rem; border-radius: 0.9rem; } .VPHero .image::before { inset: 6% 4% -4% 6%; transform: rotate(-2deg); } .VPHero .image::after, .VPHero .image-container::before, .VPHero .image-container::after { display: none; } .VPFeatures { padding-top: 1rem !important; } } @keyframes heroFloat { 0%, 100% { transform: translate3d(0, 0, 0); } 50% { transform: translate3d(0, 18px, 0); } } @keyframes sideCardLeft { 0%, 100% { transform: rotateX(10deg) rotateY(22deg) rotateZ(-10deg) translateY(0) scale(0.9); } 50% { transform: rotateX(8deg) rotateY(18deg) rotateZ(-8deg) translateY(10px) scale(0.92); } } @keyframes sideCardRight { 0%, 100% { transform: rotateX(2deg) rotateY(-18deg) rotateZ(8deg) translateY(0) scale(0.86); } 50% { transform: rotateX(4deg) rotateY(-14deg) rotateZ(6deg) translateY(-8px) scale(0.88); } } @keyframes glowSweep { 0%, 100% { background-position: center, 140% 0; } 50% { background-position: center, -40% 0; } } /* Lightbox Styles */ .lightbox-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.85); display: flex; justify-content: center; align-items: center; z-index: 9999; backdrop-filter: blur(8px); cursor: zoom-out; animation: fadeIn 0.3s ease; } .lightbox-img { max-width: 90vw; max-height: 90vh; object-fit: contain; box-shadow: 0 0 40px rgba(0,0,0,0.5); transform-origin: center; animation: zoomIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes zoomIn { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } } .close-btn { position: absolute; top: 20px; right: 40px; color: white; font-size: 48px; font-weight: 200; cursor: pointer; line-height: 1; transition: transform 0.2s ease; } .close-btn:hover { transform: scale(1.1); } </style>
×