optimization

This commit is contained in:
esef committed 2026-09-23 17:04:02 +03:00
1 parent 9dd5b25f10
commit f4457c5c17
10 files changed
+232 -46

No files matched your search

+6 -4
View File
@@ -4,7 +4,7 @@
```
website/
├── index.html # Single-page entry point — all 5 tabs live here
├── index.html # Single-page entry point — all 6 tabs live here
├── _redirects # Netlify SPA rewrite rule (/* → /index.html)
├── js/
│ ├── tabs.js # Client-side routing, tab switching, background video swap
@@ -19,9 +19,10 @@ website/
│ ├── yuukabot.css # Dashboard glassmorphism cards + grid
│ ├── calendar.css # Calendar grid, popups, sidebar
│ ├── bucketlist.css # Lock screen + list panel
│ ├── services.css # Services tab — one .svc-banner per service (Bot Status look)
│ └── gallery.css # Gallery grid + lightbox
├── gallery/ # Full-quality images, thumbnails/, and manifest.json
├── textures/ # Background MP4 videos + clickable character PNG
├── textures/ # Background MP4s, clickable character PNG, service logos
├── sounds/ # Click SFX (yuuka_click.mp3)
├── generate_thumbnails.py # Pillow script: builds thumbnails/ + manifest.json
└── firebase.txt # Firestore security rules (reference only, gitignored)
@@ -44,7 +45,8 @@ npx serve .
```
- `_redirects` handles SPA routing on Netlify; for local dev, navigate to `/index.html` directly.
- After adding images to `gallery/`, run `python generate_thumbnails.py` (requires `Pillow`). It creates missing thumbnails and rewrites `gallery/manifest.json`, which is the list the gallery renders — an image not in the manifest won't show up.
- After adding images to `gallery/`, run `python generate_thumbnails.py` (requires `Pillow`, and `ffmpeg` for GIFs). It converts animated GIFs to much smaller MP4s, creates missing thumbnails, and rewrites `gallery/manifest.json`, which is the list the gallery renders — an image not in the manifest won't show up.
- **Keep data usage low.** Background videos are 1080p H.264, no audio, `+faststart`, `preload="none"` (tabs.js starts the one the current tab needs). Recent ones use `ffmpeg -i in.mp4 -r 30 -c:v libx264 -preset slow -crf 28 -pix_fmt yuv420p -an -movflags +faststart out.mp4`. Keep untouched originals outside the site folder (`../media_originals/`). Size images to about 2× their displayed size.
There are **no automated tests** in this project.
@@ -83,7 +85,7 @@ Examples from the repo: `calendar added`, `fix upcoming filter`, `css polish bs`
1. Add the new `<section class="tab-content" id="…">` in `index.html`.
2. Add a `<button id="…-btn">` to the nav bar (`#top-line`).
3. Add the tab name to the `valid` array in `js/tabs.js`.
3. Add the tab name to the `valid` array in `js/tabs.js`. For a custom background video, map the tab to a video element in `tabVideos` (tabs can share one) and optionally add a nav theme class.
4. Create a dedicated stylesheet in `style/` and JS module in `js/` if needed.
5. Link both in `index.html` `<head>` (CSS) and before `</body>` (JS).
6. If the feature reads/writes Firestore, add the matching security rules to `firebase.txt`.
+10 -10
View File
@@ -26,15 +26,15 @@
"25.jpg",
"26.jpg",
"27.gif",
"28.gif",
"29.gif",
"30.gif",
"31.gif",
"32.gif",
"33.gif",
"34.gif",
"35.gif",
"36.gif",
"37.gif",
"28.mp4",
"29.mp4",
"30.mp4",
"31.mp4",
"32.mp4",
"33.mp4",
"34.mp4",
"35.mp4",
"36.mp4",
"37.mp4",
"38.png"
]
+71 -21
View File
@@ -3,17 +3,23 @@
Run after adding images to gallery/:
python generate_thumbnails.py
- Creates gallery/thumbnails/<name>.jpg (max 400x400) for any image missing one.
- Converts animated GIFs to MP4 (needs ffmpeg on PATH). Same animation at a
few % of the size; the GIF stays as the source but the site uses the MP4.
- Creates gallery/thumbnails/<name>.jpg (max 400x400) for any item missing one.
- Rewrites gallery/manifest.json: the ordered list of full-size filenames
that js/gallery.js renders.
that js/gallery.js renders (the MP4 wins when a GIF has one).
"""
import json
import os
import shutil
import subprocess
from PIL import Image
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif"}
VIDEO_EXTS = {".mp4"}
THUMB_SIZE = (400, 400)
FFMPEG = shutil.which("ffmpeg")
def sort_key(filename):
@@ -22,48 +28,91 @@ def sort_key(filename):
return (0, int(stem), "") if stem.isdigit() else (1, 0, stem.lower())
def list_images(base_folder):
def list_files(base_folder, exts):
return sorted(
(f for f in os.listdir(base_folder)
if os.path.isfile(os.path.join(base_folder, f))
and os.path.splitext(f)[1].lower() in IMAGE_EXTS),
and os.path.splitext(f)[1].lower() in exts),
key=sort_key,
)
def create_thumbnails(base_folder, images):
def convert_gifs(base_folder):
if not FFMPEG:
print("ffmpeg not found — skipping GIF -> MP4 conversion")
return
for filename in list_files(base_folder, {".gif"}):
src = os.path.join(base_folder, filename)
dst = os.path.join(base_folder, os.path.splitext(filename)[0] + ".mp4")
if os.path.exists(dst):
continue
with Image.open(src) as img:
if getattr(img, "n_frames", 1) < 2:
continue # still image, keep as GIF
subprocess.run([
FFMPEG, "-hide_banner", "-loglevel", "error", "-y", "-i", src,
"-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", # h264 needs even sizes
"-c:v", "libx264", "-crf", "23", "-pix_fmt", "yuv420p",
"-an", "-movflags", "+faststart", dst,
], check=True)
print(f"Converted {filename} -> {os.path.basename(dst)}")
def make_thumbnail(src, thumb_path):
if os.path.splitext(src)[1].lower() in VIDEO_EXTS:
if not FFMPEG:
raise RuntimeError("ffmpeg needed for video thumbnails")
subprocess.run([
FFMPEG, "-hide_banner", "-loglevel", "error", "-y", "-i", src,
"-frames:v", "1", "-vf", "scale='min(400,iw)':'min(400,ih)':force_original_aspect_ratio=decrease",
"-q:v", "4", thumb_path,
], check=True)
return
with Image.open(src) as img:
# JPEG needs RGB (e.g. for RGBA PNGs or palette GIFs)
if img.mode != "RGB":
img = img.convert("RGB")
img.thumbnail(THUMB_SIZE) # keeps aspect ratio
img.save(thumb_path, "JPEG", quality=85)
def create_thumbnails(base_folder, files):
thumb_dir = os.path.join(base_folder, "thumbnails")
os.makedirs(thumb_dir, exist_ok=True)
count = 0
for filename in images:
for filename in files:
# Always save thumbnail as jpg
stem = os.path.splitext(filename)[0]
thumb_path = os.path.join(thumb_dir, f"{stem}.jpg")
if os.path.exists(thumb_path):
continue
try:
with Image.open(os.path.join(base_folder, filename)) as img:
# JPEG needs RGB (e.g. for RGBA PNGs or palette GIFs)
if img.mode != "RGB":
img = img.convert("RGB")
img.thumbnail(THUMB_SIZE) # keeps aspect ratio
img.save(thumb_path, "JPEG", quality=85)
count += 1
print(f"Created thumbnail for {filename}")
make_thumbnail(os.path.join(base_folder, filename), thumb_path)
count += 1
print(f"Created thumbnail for {filename}")
except Exception as e:
print(f"Failed to process {filename}: {e}")
print(f"Created {count} thumbnails in {base_folder}")
def write_manifest(base_folder, images):
def pick_for_site(files):
# One entry per name; prefer the MP4 over its source GIF
chosen = {}
for f in files:
stem, ext = os.path.splitext(f)
if stem not in chosen or ext.lower() in VIDEO_EXTS:
chosen[stem] = f
return sorted(chosen.values(), key=sort_key)
def write_manifest(base_folder, files):
path = os.path.join(base_folder, "manifest.json")
with open(path, "w", encoding="utf-8") as f:
json.dump(images, f, indent=2)
json.dump(files, f, indent=2)
f.write("\n")
print(f"Wrote {path} ({len(images)} images)")
print(f"Wrote {path} ({len(files)} items)")
if __name__ == "__main__":
@@ -71,6 +120,7 @@ if __name__ == "__main__":
if not os.path.isdir(folder):
print(f"Folder {folder} does not exist.")
else:
imgs = list_images(folder)
create_thumbnails(folder, imgs)
write_manifest(folder, imgs)
convert_gifs(folder)
site_files = pick_for_site(list_files(folder, IMAGE_EXTS | VIDEO_EXTS))
create_thumbnails(folder, site_files)
write_manifest(folder, site_files)
+23 -2
View File
@@ -9,12 +9,13 @@
<link rel="stylesheet" href="style/yuukabot.css">
<link rel="stylesheet" href="style/calendar.css">
<link rel="stylesheet" href="style/bucketlist.css">
<link rel="stylesheet" href="style/services.css">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<title>Esef</title>
</head>
<body>
<video autoplay muted loop id="background-video">
<video muted loop playsinline preload="none" id="background-video">
<source src="textures/Yuuka_body.mp4" type="video/mp4">
</video>
<video muted loop playsinline preload="none" id="calendar-background-video" style="display:none">
@@ -32,6 +33,7 @@
<button id="bucketlist-btn">Bucketlist</button>
<button id="yuukabot-btn">Bot Status</button>
<button id="calendar-btn">Calendar</button>
<button id="services-btn">Services</button>
</div>
<section id="about" class="tab-content">
@@ -39,7 +41,7 @@
<!-- Top half: welcome -->
<div id="about-hero">
<div id="click-counter-container">
<img src="textures/yuuka_click.png" id="yuuka-click-btn" alt="Click Me!">
<img src="textures/yuuka_click.webp" id="yuuka-click-btn" alt="Click Me!">
<div id="click-counter-display">...</div>
<div id="click-counter-label">click me</div>
</div>
@@ -205,10 +207,29 @@
</div>
</section>
<section id="services" class="tab-content">
<header class="svc-header">
<h1>Services</h1>
</header>
<div class="svc-list">
<!-- One banner per service — copy this block to add another -->
<article class="svc-banner" style="--brand: #FFD600">
<img class="svc-logo" src="textures/revolt_logo.png" alt="">
<div class="svc-info">
<h2 class="svc-name">ReVoltVPN</h2>
<p class="svc-desc">Free, open-source VPN for Android. No sign-up, no logs.</p>
</div>
<a class="svc-btn" href="https://userevolt.app" target="_blank" rel="noopener">Visit &rarr;</a>
</article>
</div>
</section>
<!-- Lightbox (gallery) -->
<div id="lightbox" class="lightbox">
<span class="lightbox-close">&times;</span>
<img class="lightbox-content" id="lightbox-img" alt="">
<video class="lightbox-content" id="lightbox-video" muted loop playsinline></video>
</div>
<script src="js/gallery.js" defer></script>
+19 -4
View File
@@ -4,22 +4,37 @@ document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('gallery-container');
const lightbox = document.getElementById('lightbox');
const lightboxImg = document.getElementById('lightbox-img');
const lightboxVideo = document.getElementById('lightbox-video');
if (!container || !lightbox) return;
// Animated GIFs are served as MP4 (a few % of the size), so those
// open in the <video>; everything else in the <img>.
const openLightbox = (src) => {
lightboxImg.src = src;
const isVideo = src.endsWith('.mp4');
lightboxImg.style.display = isVideo ? 'none' : 'block';
lightboxVideo.style.display = isVideo ? 'block' : 'none';
if (isVideo) {
lightboxVideo.src = src;
lightboxVideo.play().catch(() => {});
} else {
lightboxImg.src = src;
}
lightbox.style.display = 'block';
};
const closeLightbox = () => {
lightbox.style.display = 'none';
lightboxImg.src = ''; // Clear source to stop GIFs
lightboxImg.removeAttribute('src');
// Stop playback and any download still in progress
lightboxVideo.pause();
lightboxVideo.removeAttribute('src');
lightboxVideo.load();
};
// Close on the X, on a click outside the image, or with Escape
// Close on the X, on a click outside the media, or with Escape
lightbox.addEventListener('click', (e) => {
if (e.target !== lightboxImg) closeLightbox();
if (e.target !== lightboxImg && e.target !== lightboxVideo) closeLightbox();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && lightbox.style.display === 'block') closeLightbox();
+11 -5
View File
@@ -9,7 +9,7 @@ document.addEventListener('DOMContentLoaded', () => {
// ── Map URL path → tab id ──────────────────────────────
function pathToTab(path) {
const slug = path.replace(/^\/+|\/+$/g, '') || 'about';
const valid = ['about', 'gallery', 'bucketlist', 'yuukabot', 'calendar'];
const valid = ['about', 'gallery', 'bucketlist', 'yuukabot', 'calendar', 'services'];
// backward compat: old /ahmet links
if (slug === 'ahmet') return 'bucketlist';
return valid.includes(slug) ? slug : 'about';
@@ -22,11 +22,12 @@ document.addEventListener('DOMContentLoaded', () => {
// ── Swap background videos + top-bar theme ────────────
const topLine = document.getElementById('top-line');
// map tab id → its dedicated video element (null = use main)
// map tab id → its dedicated video element (unlisted = use main)
const tabVideos = {
calendar: calVideo,
bucketlist: bucketlistVideo,
yuukabot: botVideo
yuukabot: botVideo,
services: botVideo
};
function setBgForTab(tabId) {
@@ -60,18 +61,23 @@ document.addEventListener('DOMContentLoaded', () => {
setBgForTab(tabId);
}
function setUrl(method, tabId) {
if (window.location.protocol === 'file:') return;
history[method]({ tab: tabId }, '', tabToPath(tabId) + window.location.search);
}
// ── Navigate to a tab (updates URL + shows it) ────────
function navigateTo(tabId) {
showTab(tabId);
window.scrollTo(0, 0);
history.pushState({ tab: tabId }, '', tabToPath(tabId));
setUrl('pushState', tabId);
}
// ── Initial load: show tab from URL ────────────────────
// (also normalises unknown / legacy paths like /ahmet in the address bar)
const initialTab = pathToTab(window.location.pathname);
showTab(initialTab);
history.replaceState({ tab: initialTab }, '', tabToPath(initialTab) + window.location.search);
setUrl('replaceState', initialTab);
// ── Tab button clicks ──────────────────────────────────
const buttons = document.querySelectorAll('#top-line button');
+92
View File
@@ -0,0 +1,92 @@
/* ═══════════════════════════════════════════════════════════
SERVICES TAB — one banner per service, Bot Status look
Add a service by copying a .svc-banner block in index.html and
setting its --brand color (used for the left stripe only).
═══════════════════════════════════════════════════════════ */
/* ── Tab header (matches .dash-header) ────────────────────── */
.svc-header {
padding: 120px 20px 30px;
text-align: center;
}
.svc-header h1 {
font-size: 2.8rem;
color: #fff;
margin: 0;
text-shadow: 0 0 30px rgba(91, 111, 222, 0.5);
}
/* ── Banner list ──────────────────────────────────────────── */
.svc-list {
display: flex;
flex-direction: column;
gap: 20px;
padding: 0 48px 60px;
}
/* ── Banner (same glass as .dash-card) ────────────────────── */
.svc-banner {
--brand: #5b6fde;
display: flex;
align-items: center;
gap: 28px;
padding: 28px 32px;
background: rgba(15, 15, 35, 0.55);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(91, 111, 222, 0.2);
border-left: 4px solid var(--brand);
border-radius: 16px;
text-align: left;
transition: border-color 0.3s, box-shadow 0.3s;
}
.svc-banner:hover {
border-color: rgba(91, 111, 222, 0.5);
border-left-color: var(--brand);
box-shadow: 0 6px 28px rgba(91, 111, 222, 0.12);
}
.svc-logo {
width: 64px;
height: 64px;
flex-shrink: 0;
border-radius: 14px;
}
.svc-info { flex: 1; min-width: 0; }
.svc-name {
margin: 0 0 6px;
font-size: 1.6rem;
color: #fff;
}
/* overrides the global p rule in styles.css */
.svc-banner .svc-desc {
max-width: none;
margin: 0;
font-size: 0.95rem;
line-height: 1.5;
color: rgba(255, 255, 255, 0.55);
font-family: Arial, Helvetica, sans-serif;
}
.svc-btn {
flex-shrink: 0;
padding: 12px 24px;
border-radius: 10px;
background: #5b6fde;
color: #fff;
font-weight: bold;
text-decoration: none;
white-space: nowrap;
transition: background 0.2s, box-shadow 0.2s;
}
.svc-btn:hover {
background: #6d80ee;
box-shadow: 0 0 18px rgba(91, 111, 222, 0.5);
}
.svc-btn:focus-visible {
outline: 2px solid #fff;
outline-offset: 3px;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB