Add JAV studios reference documentation and various UI improvements

- Add comprehensive JAV studios quick reference guide
- Update documentation index with JAV reference
- Add logo animation components and test files
- Update CSS styling for cards, buttons, forms, and theme
- Add utility scripts for configuration and import workflows
- Update templates and UI components

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Stu Leak 2025-12-28 16:36:38 -05:00
parent 073fc49745
commit 2b4a2038fa
29 changed files with 1154 additions and 184 deletions

View File

@ -22,6 +22,8 @@ Goondex is a fast, local-first media indexer for adult content. It ingests metad
### Integration
- [TPDB Integration](TPDB_INTEGRATION.md) - ThePornDB API integration guide
- [Adult Empire Scraper](ADULT_EMPIRE_SCRAPER.md) - Adult Empire scraper implementation
- [JAV Studios Reference](JAV_STUDIOS_REFERENCE.md) - Japanese Adult Video studios quick reference
- [Scraper System](SCRAPER_SYSTEM.md) - How scrapers work
- [Adding New Sources](ADDING_SOURCES.md) - Implementing new data sources

View File

@ -0,0 +1,74 @@
# JAV Studios Quick Reference
**Last Updated:** December 28, 2025
**Status:** Planning phase - for future JAV scraper implementation
This document provides a quick reference for Japanese Adult Video (JAV) studios, their code patterns, specialties, and websites. This information will be used when implementing JAV scrapers for Goondex.
---
## Uncensored Studios (No Mosaic)
| Studio | Code/Abbrev | Specialties | Website |
|-----------------|-----------------|--------------------------------------|----------------------------------|
| FC2 PPV | FC2PPV | Amateur, creampie, gyaru | adult.fc2.com |
| 1pondo | 1Pondo | High-prod, GF roleplay, creampie | 1pondo.tv |
| Caribbeancom | Caribbeancom | MILF, amateur, big tits, anal | caribbeancom.com |
| HEYZO | HEYZO | Mature, taboo, creampie | en.heyzo.com |
| Pacopacomama | Pacopacomama | Mature housewife, sensual | pacopacomama.com |
| Tokyo Hot | Tokyo Hot | Hardcore, gangbang, extreme | tokyo-hot.com |
| 10musume | 10musume | Real amateurs, pickup | 10musume.com |
---
## Censored Studios (Mosaic Required)
| Studio | Code/Abbrev | Specialties | Website |
|-----------------|-----------------|--------------------------------------|----------------------------------|
| Moodyz | MIAA, MIDE | Variety, drama, idol, creampie | moodyz.com |
| S1 No.1 Style | SONE, SSIS | Luxury idols, high production | s1s1s1.com |
| Prestige | ABP, ABW | Amateur-style, POV, beautiful girls | prestige-av.com |
| Idea Pocket | IPZZ, IPX | Beautiful idols, aesthetics | ideapocket.com |
| SOD Create | SDDE, SDMU | Variety, gimmick, experimental | sod.co.jp |
| Madonna | JUQ, JUX | Mature housewife, drama | madonna-av.com |
| Attackers | RBD, SHKD | Hardcore, intense, dark drama | attackers.net |
| Fitch | JUFD | Mature, big tits | fitch-av.com |
---
## Notes for Scraper Implementation
### Code Pattern Recognition
JAV studios use consistent code patterns for their releases:
- **Uncensored:** Often use studio-specific codes (e.g., FC2PPV-XXXXXX, 1Pondo-XXXXXX)
- **Censored:** Typically use letter codes followed by numbers (e.g., SSIS-XXX, MIAA-XXX)
### Important Considerations
1. **Censorship Status:** Track whether content is censored or uncensored in the database
2. **Multiple Codes:** Some studios use multiple code prefixes (e.g., Moodyz uses MIAA, MIDE, etc.)
3. **Code Evolution:** Studio codes may change over time as branding evolves
4. **Website Access:** Some sites may require region-specific access or age verification
### Future Scraper Architecture
When implementing JAV scrapers:
- Create separate scraper modules for each major studio
- Implement code pattern matching for automatic studio detection
- Handle both censored and uncensored content appropriately
- Consider rate limiting for scraper requests to avoid blocking
- Implement metadata standardization across different studios
---
## Related Documentation
- [ARCHITECTURE.md](ARCHITECTURE.md) - Overall system architecture
- [DATABASE_SCHEMA.md](DATABASE_SCHEMA.md) - Database schema including scene metadata
- [ADULT_EMPIRE_SCRAPER.md](ADULT_EMPIRE_SCRAPER.md) - Example scraper implementation
- [TPDB_INTEGRATION.md](TPDB_INTEGRATION.md) - TPDB integration patterns
---
**Note:** For full details and current information, always refer to official studio websites. This is a quick reference guide only.

View File

@ -1,13 +1,14 @@
# Goondex TODO / DONE
## TODO
- [ ] Implement bulk studio import (`./goondex import all-studios`) with the same pagination/resume flow as the performer importer.
- [ ] Implement bulk scene import (`./goondex import all-scenes`) and wire the CLI/UI to the new data set.
## TODO (v0.1.0-dev4+)
- [ ] Add image ingestion pipeline (WebP downscale, cached thumbs) for performers (multi-image support) and scenes; make it non-blocking with concurrency caps.
- [ ] Add image backfill/enrichment command for performers/scenes (fetch missing thumbs, skip existing).
- [ ] Build a movie ingest path (TPDB and/or Adult Empire) that feeds the `movies` tables and populates the movies pages.
- [ ] Align the web stack on a single CSS pipeline (deprecate legacy `style.css`, keep goondex + scoped component files).
- [ ] Add lightweight UI validation (lint/smoke tests) for navigation, modals, and search to catch regressions early.
## DONE
- [x] Bulk performer/studio/scene imports paginate until empty (ignore TPDB 10k cap) to maximize coverage.
- [x] Split card styling into per-context files (base, performers, studios, scenes) and updated listing templates to use them.
- [x] Created shared task lists (`docs/TODO.md`, `docs/WEB_TODO.md`) to keep engineering and web work in sync.
- [x] Adult Empire scraper + TPDB merge support for performers (see `SESSION_SUMMARY_v0.1.0-dev4.md`).

View File

@ -74,6 +74,15 @@ func (s *Service) BulkImportAllPerformersWithProgress(ctx context.Context, progr
// Update total on first page
if meta != nil && page == 1 {
result.Total = meta.Total
if meta.Total >= 10000 {
log.Printf("TPDB performers total reports %d (cap?). Continuing to paginate until empty.", meta.Total)
}
}
// Stop when no data is returned
if len(performers) == 0 {
log.Printf("No performers returned at page %d; stopping import.", page)
break
}
// Import each performer
@ -102,11 +111,6 @@ func (s *Service) BulkImportAllPerformersWithProgress(ctx context.Context, progr
log.Printf("Imported page %d/%d of performers (%d/%d total)", page, meta.LastPage, result.Imported, result.Total)
// Check if we've reached the last page
if meta == nil || page >= meta.LastPage {
break
}
page++
}
@ -136,6 +140,14 @@ func (s *Service) BulkImportAllStudiosWithProgress(ctx context.Context, progress
// Update total on first page
if meta != nil && page == 1 {
result.Total = meta.Total
if meta.Total >= 10000 {
log.Printf("TPDB studios total reports %d (cap?). Continuing to paginate until empty.", meta.Total)
}
}
if len(studios) == 0 {
log.Printf("No studios returned at page %d; stopping import.", page)
break
}
// Import each studio
@ -161,11 +173,6 @@ func (s *Service) BulkImportAllStudiosWithProgress(ctx context.Context, progress
log.Printf("Imported page %d/%d of studios (%d/%d total)", page, meta.LastPage, result.Imported, result.Total)
// Check if we've reached the last page
if meta == nil || page >= meta.LastPage {
break
}
page++
}
@ -198,6 +205,14 @@ func (s *Service) BulkImportAllScenesWithProgress(ctx context.Context, progress
// Update total on first page
if meta != nil && page == 1 {
result.Total = meta.Total
if meta.Total >= 10000 {
log.Printf("TPDB scenes total reports %d (cap?). Continuing to paginate until empty.", meta.Total)
}
}
if len(scenes) == 0 {
log.Printf("No scenes returned at page %d; stopping import.", page)
break
}
// Import each scene with its performers and tags
@ -279,11 +294,6 @@ func (s *Service) BulkImportAllScenesWithProgress(ctx context.Context, progress
log.Printf("Imported page %d/%d of scenes (%d/%d total)", page, meta.LastPage, result.Imported, result.Total)
// Check if we've reached the last page
if meta == nil || page >= meta.LastPage {
break
}
page++
}

View File

@ -19,23 +19,20 @@
font-weight: 600;
cursor: pointer;
color: var(--color-text-primary);
background: var(--color-bg-elevated);
color: #fff;
background: var(--color-brand);
border: 1px solid var(--color-border-soft);
border: 1px solid var(--color-brand);
transition: background var(--transition),
border-color var(--transition),
box-shadow var(--transition),
transform var(--transition-fast);
}
/* Hover glow (SUBTLE, medium intensity) */
.btn:hover {
background: var(--color-bg-card);
border-color: var(--color-brand);
box-shadow: var(--shadow-glow-pink-soft);
transform: translateY(-2px);
background: var(--color-brand-hover);
border-color: var(--color-brand-hover);
transform: none;
}
/* Active press */
@ -58,21 +55,16 @@
.btn-primary,
.btn.brand,
.btn.pink {
background: linear-gradient(
135deg,
var(--color-brand) 0%,
var(--color-brand-hover) 90%
);
background: linear-gradient(135deg, var(--color-brand), var(--color-brand-hover));
border: none;
color: #fff;
text-shadow: 0 0 8px rgba(255, 255, 255, 0.25);
text-shadow: none;
}
.btn-primary:hover,
.btn.brand:hover,
.btn.pink:hover {
box-shadow: var(--shadow-glow-pink);
transform: translateY(-2px);
transform: none;
}
@ -80,15 +72,30 @@
* SECONDARY BUTTON
* ================================ */
.btn-secondary {
background: var(--color-bg-card);
border: 1px solid var(--color-border-soft);
color: var(--color-text-primary);
background: transparent;
border: 2px solid var(--color-brand);
color: var(--color-brand);
}
.btn-secondary:hover {
border-color: var(--color-brand);
border-color: var(--color-brand-hover);
color: var(--color-brand-hover);
}
/* ================================
* LIGHT PRIMARY (white bg, pink text)
* ================================ */
.btn-light-primary {
background: #ffffff;
color: var(--color-brand);
box-shadow: var(--shadow-glow-pink-soft);
border: none;
}
.btn-light-primary:hover {
background: #ffffff;
color: var(--color-brand-hover);
border: none;
transform: none;
}
@ -102,7 +109,7 @@
}
.btn-small:hover {
transform: translateY(-1px);
transform: none;
}

View File

@ -17,39 +17,24 @@
.gx-card {
background: var(--color-bg-card);
border: 1px solid var(--color-border-soft);
border-radius: var(--radius-soft);
border: 1px solid var(--color-border);
border-radius: 20px;
overflow: hidden;
box-shadow: var(--shadow-elevated);
transition:
transform var(--transition),
box-shadow var(--transition),
border-color var(--transition);
box-shadow: none;
transition: none;
cursor: pointer;
position: relative;
}
.gx-card:hover {
transform: translateY(-4px);
border-color: var(--color-brand);
box-shadow:
0 0 18px rgba(255, 79, 163, 0.28),
0 6px 24px rgba(0, 0, 0, 0.55);
}
.gx-card-thumb {
width: 100%;
aspect-ratio: var(--gx-card-thumb-ratio);
background-size: cover;
background-position: center;
filter: brightness(0.92);
transition: filter var(--transition-fast);
}
.gx-card:hover .gx-card-thumb {
filter: brightness(1);
filter: none;
transition: none;
}
.gx-card-body {
@ -62,10 +47,7 @@
.gx-card-title {
font-size: 1.1rem;
font-weight: 600;
background: linear-gradient(135deg, var(--color-text-primary), var(--color-header));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
color: var(--color-text-primary);
}
.gx-card-meta {
@ -84,10 +66,10 @@
.gx-card-tag {
padding: 0.2rem 0.55rem;
font-size: 0.75rem;
border-radius: var(--radius);
background: rgba(255, 79, 163, 0.08);
border-radius: 12px;
background: rgba(255, 79, 163, 0.15);
color: var(--color-brand);
border: 1px solid rgba(255, 79, 163, 0.25);
border: 1px solid rgba(255, 79, 163, 0.3);
text-transform: uppercase;
letter-spacing: 0.03em;
}

View File

@ -14,3 +14,21 @@
.performer-card .gx-card-tags {
margin-top: 0.6rem;
}
/* Harsh pink style reserved for performer cards */
.performer-card .gx-card {
background: var(--color-brand);
color: #ffffff;
border: 5px solid #ffffff;
}
.performer-card .gx-card-title,
.performer-card .gx-card-meta,
.performer-card .gx-card-tag {
color: #ffffff;
}
.performer-card .gx-card-tag {
background: rgba(255, 255, 255, 0.12);
border: 1px solid #ffffff;
}

View File

@ -14,3 +14,21 @@
.scene-card .gx-card-tags {
margin-top: 0.6rem;
}
/* Harsh pink style reserved for scene cards */
.scene-card .gx-card {
background: var(--color-brand);
color: #ffffff;
border: 5px solid #ffffff;
}
.scene-card .gx-card-title,
.scene-card .gx-card-meta,
.scene-card .gx-card-tag {
color: #ffffff;
}
.scene-card .gx-card-tag {
background: rgba(255, 255, 255, 0.12);
border: 1px solid #ffffff;
}

View File

@ -9,16 +9,11 @@
* ============================================ */
.card {
background: var(--color-bg-card);
border: 1px solid var(--color-border-soft);
border-radius: var(--radius);
border: 1px solid var(--color-border);
border-radius: 20px;
padding: 1.5rem;
box-shadow: var(--shadow-elevated);
transition: background var(--transition), box-shadow var(--transition);
}
.card:hover {
background: var(--color-bg-elevated);
box-shadow: var(--shadow-glow-pink-soft);
box-shadow: none;
transition: none;
}
/* ============================================
@ -26,26 +21,21 @@
* ============================================ */
.stat-card {
background: var(--color-bg-card);
border-radius: var(--radius);
border-radius: 20px;
padding: 1.5rem;
display: flex;
align-items: center;
gap: 1.2rem;
border: 1px solid var(--color-border-soft);
box-shadow: var(--shadow-elevated);
transition: transform var(--transition), box-shadow var(--transition);
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-glow-pink);
border: 1px solid var(--color-border);
box-shadow: none;
transition: none;
}
.stat-icon {
font-size: 2.2rem;
color: var(--color-brand);
text-shadow: 0 0 10px var(--color-brand-glow);
text-shadow: none;
}
.stat-content {
@ -86,9 +76,9 @@
.search-results {
margin-top: 0.75rem;
background: var(--color-bg-card);
border: 1px solid var(--color-border-soft);
border-radius: var(--radius);
box-shadow: var(--shadow-elevated);
border: 1px solid var(--color-border);
border-radius: 20px;
box-shadow: none;
max-height: 340px;
overflow-y: auto;
padding: 0.5rem;
@ -96,13 +86,9 @@
.search-result-item {
padding: 0.75rem 1rem;
border-radius: var(--radius);
border-radius: 12px;
cursor: pointer;
transition: background var(--transition);
}
.search-result-item:hover {
background: rgba(255, 79, 163, 0.08);
transition: none;
}
.search-result-title {
@ -227,4 +213,3 @@
transparent
);
}

View File

@ -31,17 +31,16 @@ select {
width: 100%;
padding: 0.9rem 1rem;
background: var(--color-bg-card);
background: var(--color-bg-elevated);
color: var(--color-text-primary);
border: 1px solid var(--color-border-soft);
border: 1px solid var(--color-border);
border-radius: var(--radius);
font-size: 1rem;
outline: none;
transition: border-color var(--transition),
box-shadow var(--transition),
background var(--transition);
}
@ -57,8 +56,7 @@ input:focus,
textarea:focus,
select:focus {
border-color: var(--color-brand);
box-shadow: 0 0 0 3px rgba(255, 79, 163, 0.18),
var(--shadow-glow-pink-soft);
box-shadow: none;
background: var(--color-bg-elevated);
}
@ -96,8 +94,8 @@ input[type="checkbox"] {
height: 18px;
border-radius: 4px;
border: 1px solid var(--color-border-soft);
background: var(--color-bg-card);
border: 1px solid var(--color-border);
background: var(--color-bg-elevated);
cursor: pointer;
position: relative;

View File

@ -8,10 +8,7 @@
* =================================== */
body {
background:
radial-gradient(1200px circle at 10% 20%, rgba(255, 79, 163, 0.10), transparent 45%),
radial-gradient(900px circle at 92% 8%, rgba(126, 231, 231, 0.08), transparent 42%),
var(--color-bg-dark);
background: var(--color-bg-dark);
min-height: 100vh;
}
@ -51,11 +48,10 @@ body {
/* Reusable elevated surface */
.surface-panel {
background: linear-gradient(135deg, rgba(255, 79, 163, 0.06), rgba(21, 21, 23, 0.92));
border: 1px solid var(--color-border-soft);
border-radius: 18px;
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: 20px;
padding: 1.75rem;
box-shadow: var(--shadow-glow-pink-soft);
}
.section-header {
@ -95,14 +91,12 @@ body {
* =================================== */
.navbar {
background: rgba(21, 21, 23, 0.92);
border-bottom: 1px solid var(--color-border-soft);
background: var(--color-bg-card);
border-bottom: 1px solid var(--color-border);
padding: 0.85rem 0;
position: sticky;
top: 0;
z-index: 40;
backdrop-filter: blur(8px);
box-shadow: var(--shadow-glow-pink-soft);
}
.nav-inner {
@ -160,37 +154,18 @@ body {
* =================================== */
.hero-section {
background: linear-gradient(
140deg,
rgba(255, 79, 163, 0.16),
rgba(12, 12, 14, 0.92)
);
border: 1px solid var(--color-border-soft);
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: 20px;
padding: 3rem 2.5rem;
margin-bottom: 2rem;
position: relative;
overflow: hidden;
box-shadow: var(--shadow-glow-pink);
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Subtle radial neon glow (G-A) */
.hero-section::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(
circle at 50% 20%,
rgba(255, 79, 163, 0.15),
rgba(255, 79, 163, 0.05) 40%,
transparent 75%
);
pointer-events: none;
}
.hero-title {
font-size: 2.8rem;
font-weight: 800;
@ -235,20 +210,14 @@ body {
.stat-card {
background: var(--color-bg-card);
border: 1px solid var(--color-border-soft);
border-radius: var(--radius);
border: 1px solid var(--color-border);
border-radius: 20px;
padding: 1.4rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
transition: transform 0.20s var(--transition),
box-shadow 0.20s var(--transition);
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-glow-pink);
transition: none;
}
.stat-icon {

View File

@ -0,0 +1,28 @@
/* Minimal bouncing animation for Goondex logo */
.goondex-logo-animated {
animation: logoBounce 2s ease-in-out infinite;
}
.goondex-logo-animated .nipple-left,
.goondex-logo-animated .nipple-right {
animation: nippleBounce 2s ease-in-out infinite;
}
.goondex-logo-animated .nipple-right {
animation-delay: 0.1s;
}
@keyframes logoBounce {
0% { transform: translateY(0) scaleY(1); }
20% { transform: translateY(-20px) scaleY(1.1); }
30% { transform: translateY(0) scaleY(0.7); }
40% { transform: translateY(8px) scaleY(1.15); }
100% { transform: translateY(0) scaleY(1); }
}
@keyframes nippleBounce {
0%, 100% { transform: translateY(0); }
25% { transform: translateY(-6px); }
50% { transform: translateY(0); }
75% { transform: translateY(-3px); }
}

View File

@ -558,6 +558,7 @@ main.container {
border: 1px solid var(--color-border);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
color: var(--color-text-primary);
@ -565,6 +566,19 @@ main.container {
justify-content: center;
}
.global-loader .logo {
display: flex;
justify-content: center;
margin-bottom: 0.5rem;
}
.global-loader .logo img,
.global-loader .logo svg {
width: 90px;
height: 55px;
filter: drop-shadow(0 2px 8px rgba(255, 95, 162, 0.3));
}
.global-loader .spinner {
width: 24px;
height: 24px;
@ -723,6 +737,17 @@ main.container {
text-decoration: underline;
}
.btn-link {
color: var(--color-brand);
text-decoration: none;
font-weight: 600;
}
.btn-link:hover {
color: var(--color-brand-hover);
text-decoration: underline;
}
.full-width {
grid-column: 1 / -1;
}

View File

@ -8,28 +8,29 @@
* =========================== */
:root {
/* --- BRAND IDENTITY --- */
--color-brand: #FF4FA3; /* Flamingo Pink (core) */
--color-brand-hover: #FF6AB7; /* Slightly brighter pink */
--color-brand-glow: rgba(255, 79, 163, 0.35); /* SUBTLE neon glow */
--color-brand: #FF4FA3; /* Flamingo Pulse Pink */
--color-brand-strong: #d74280; /* Deep Flamingo (new) */
--color-brand-hover: #d74280; /* Hover uses deeper pink */
--color-brand-glow: transparent; /* Flat theme: no glow */
/* --- TEXT --- */
--color-text-primary: #F5F5F7;
--color-text-secondary: #A0A3AB;
--color-header: #E08FEA;
--color-keypoint: #FF6ACB;
--color-text-primary: #F8F8F8;
--color-text-secondary: #9BA0A8;
--color-header: #D78BE0;
--color-keypoint: #FF66C4;
/* --- ALERTS --- */
--color-warning: #FFAA88;
--color-info: #7EE7E7;
/* --- BACKGROUND LAYERS (dark only) --- */
--color-bg-dark: #0A0A0C;
--color-bg-card: #151517;
--color-bg-elevated: #212124;
/* --- BACKGROUND LAYERS (plum-forward dark) --- */
--color-bg-dark: #2f2333; /* Plum base */
--color-bg-card: #3a2b40; /* Card plum */
--color-bg-elevated: #44344a; /* Elevated plum */
/* --- BORDERS --- */
--color-border: #3d3d44;
--color-border-soft: rgba(255, 79, 163, 0.15); /* Flamingo soft border */
--color-border: #59475f;
--color-border-soft: #59475f;
/* --- RADII --- */
--radius: 12px;
@ -42,10 +43,10 @@
/* --- UI GRID --- */
--rail-width: 180px;
/* --- GLOWS + SHADOWS (medium intensity only) --- */
--shadow-glow-pink: 0 0 18px rgba(255, 79, 163, 0.28);
--shadow-glow-pink-soft: 0 0 38px rgba(255, 79, 163, 0.14);
--shadow-elevated: 0 6px 22px rgba(0, 0, 0, 0.6);
/* --- SHADOWS (flattened) --- */
--shadow-glow-pink: none;
--shadow-glow-pink-soft: none;
--shadow-elevated: none;
}
/* ===========================
@ -82,12 +83,12 @@ body {
::-webkit-scrollbar-thumb {
background: var(--color-brand);
border-radius: 6px;
box-shadow: var(--shadow-glow-pink-soft);
box-shadow: none;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-brand-hover);
box-shadow: var(--shadow-glow-pink);
box-shadow: none;
}
/* ===========================
@ -105,22 +106,38 @@ body {
/* Subtle glowing border */
.glow-border {
border: 1px solid var(--color-border-soft);
box-shadow: var(--shadow-glow-pink-soft);
box-shadow: none;
}
/* Card elevation */
.elevated {
background: var(--color-bg-elevated);
box-shadow: var(--shadow-elevated);
box-shadow: none;
}
/* Brand glow text (subtle) */
.text-glow {
text-shadow: 0 0 12px var(--color-brand-glow);
text-shadow: none;
}
/* Pink glow panel (subtle accent for navbar or hero) */
.panel-glow {
box-shadow: inset 0 0 60px rgba(255, 79, 163, 0.08),
0 0 22px rgba(255, 79, 163, 0.20);
box-shadow: none;
}
/* Global flat override to strip remaining glow from legacy components */
body, header, footer, nav, section, article,
.card, .panel, .navbar, .sidebar, .btn, .button, .badge, .chip, .tag,
input, select, textarea, button,
.modal, .dialog, .tooltip, .toast, .dropdown, .tabs, .table {
box-shadow: none !important;
text-shadow: none !important;
filter: none !important;
}
/* Absolute kill-switch for any remaining glow/shadow */
*, *::before, *::after {
box-shadow: none !important;
text-shadow: none !important;
filter: none !important;
}

View File

@ -27,9 +27,9 @@
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
inkscape:zoom="1.4142136"
inkscape:cx="299.10616"
inkscape:cy="244.65894"
inkscape:zoom="2.0000001"
inkscape:cx="205.49999"
inkscape:cy="240.49999"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
@ -485,7 +485,13 @@
id="tspan49"
x="231.02339"
y="414.16824"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:18.6667px;font-family:'Gmarket Sans';-inkscape-font-specification:'Gmarket Sans, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#262626;fill-opacity:1">86</tspan></text></g></g></g><g
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:18.6667px;font-family:'Gmarket Sans';-inkscape-font-specification:'Gmarket Sans, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#262626;fill-opacity:1">86</tspan></text></g></g><rect
style="opacity:1;fill:#262626;fill-opacity:1"
id="rect50"
width="235"
height="65"
x="16.79899"
y="-80.771034" /></g><g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="performer_info 1"

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.9 MiB

View File

@ -5,14 +5,45 @@ function openModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add('active');
}
}
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove('active');
// ============================================================================
// Logo Animation for Loading Screens
// ============================================================================
let logoAnimator = null;
function startLogoAnimation() {
// Find logo in loader or main content
const logoElement = document.querySelector('#global-loader .logo img,
#global-loader .logo svg,
.logo img, .logo svg');
if (logoElement && !logoAnimator) {
// Add CSS if not already loaded
if (!document.querySelector('#logo-animation-css')) {
const css = document.createElement('link');
css.id = 'logo-animation-css';
css.rel = 'stylesheet';
css.href = '/static/css/logo-animation.css';
document.head.appendChild(css);
}
// Initialize animator
logoAnimator = new LogoAnimator();
logoAnimator.init(logoElement);
logoAnimator.startBounce();
}
}
function stopLogoAnimation() {
if (logoAnimator) {
logoAnimator.stopBounce();
logoAnimator = null;
}
}
}
// Import functions
@ -469,6 +500,8 @@ function showLoader(msg) {
const text = document.getElementById('global-loader-text');
if (overlay) {
overlay.style.display = 'flex';
// Start logo animation when loader shows
startLogoAnimation();
}
if (text && msg) {
text.textContent = msg;
@ -477,7 +510,11 @@ function showLoader(msg) {
function hideLoader() {
const overlay = document.getElementById('global-loader');
if (overlay) overlay.style.display = 'none';
if (overlay) {
overlay.style.display = 'none';
// Stop logo animation when loader hides
stopLogoAnimation();
}
}
// Unified SSE import helper with progress bar

View File

@ -0,0 +1,103 @@
class LogoAnimator {
constructor() {
this.isAnimating = false;
this.logoElement = null;
}
init(svgElement) {
this.logoElement = svgElement;
this.identifyParts();
}
identifyParts() {
if (!this.logoElement) return;
const nipples = [];
const breasts = [];
const breastCandidates = [
this.logoElement.querySelector('#breast-left'),
this.logoElement.querySelector('#breast-right')
].filter(Boolean);
const nippleCandidates = [
this.logoElement.querySelector('#nipple-left'),
this.logoElement.querySelector('#nipple-right')
].filter(Boolean);
breasts.push(...breastCandidates);
nipples.push(...nippleCandidates);
if (nipples.length < 2) {
const circ = Array.from(this.logoElement.querySelectorAll('circle, ellipse'));
while (nipples.length < 2 && circ.length) nipples.push(circ.shift());
}
if (breasts.length < 2) {
const shapes = Array.from(this.logoElement.querySelectorAll('path, polygon, rect'));
while (breasts.length < 2 && shapes.length) breasts.push(shapes.shift());
}
if (breasts.length === 0) breasts.push(this.logoElement);
if (breasts.length === 1) breasts.push(this.logoElement);
if (breasts[0]) breasts[0].classList.add('breast-left');
if (breasts[1]) breasts[1].classList.add('breast-right');
if (nipples.length === 0) nipples.push(breasts[0], breasts[1]);
nipples.slice(0, 2).forEach((el, idx) => el && el.classList.add(idx === 0 ? 'nipple-left' : 'nipple-right'));
}
startBounce() {
if (!this.logoElement || this.isAnimating) return;
this.logoElement.classList.add('goondex-logo-animated');
this.isAnimating = true;
}
stopBounce() {
if (!this.logoElement) return;
this.logoElement.classList.remove('goondex-logo-animated');
this.isAnimating = false;
}
}
async function loadSVG(urls, targetId) {
const target = document.getElementById(targetId);
if (!target) return null;
for (const url of urls) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error('fetch failed');
const svgText = await res.text();
target.innerHTML = svgText;
const svg = target.querySelector('svg');
return svg;
} catch (e) {
continue;
}
}
// Fallback to img if all fetches fail
target.innerHTML = `<img src="${urls[0]}" alt="Goondex Logo" width="100%" height="100%">`;
return null;
}
(async function initLogoAnim() {
const logoURLs = [
"/static/img/logo/GOONDEX_Titty.svg",
"http://localhost:8788/static/img/logo/GOONDEX_Titty.svg",
];
const staticSvg = await loadSVG(logoURLs, 'static-logo');
const animatedSvg = await loadSVG(logoURLs, 'animated-logo');
const loaderSvg = await loadSVG(logoURLs, 'loader-logo');
window.goondexLogoAnim = { animator: null, loaderAnimator: null };
if (animatedSvg) {
const animator = new LogoAnimator();
animator.init(animatedSvg);
animator.startBounce();
window.goondexLogoAnim.animator = animator;
}
if (loaderSvg) {
const l = new LogoAnimator();
l.init(loaderSvg);
window.goondexLogoAnim.loaderAnimator = l;
}
})();

View File

@ -0,0 +1,58 @@
// Minimal logo animation controller
class LogoAnimator {
constructor() {
this.isAnimating = false;
this.logoElement = null;
}
// Initialize with SVG element
init(svgElement) {
this.logoElement = svgElement;
this.identifyNipples();
}
// Identify nipple elements by their circular paths
identifyNipples() {
if (!this.logoElement) return;
const paths = this.logoElement.querySelectorAll('path');
let nippleIndex = 0;
paths.forEach((path) => {
const d = path.getAttribute('d');
// Look for the specific circular nipple paths in the GOONDEX_Titty.svg
if (d && d.includes('1463.5643,67.636337')) {
path.classList.add('nipple-left');
nippleIndex++;
} else if (d && d.includes('70.4489,0') && nippleIndex === 1) {
path.classList.add('nipple-right');
nippleIndex++;
}
});
}
// Start bouncing animation
startBounce() {
if (!this.logoElement || this.isAnimating) return;
this.logoElement.classList.add('goondex-logo-animated');
this.isAnimating = true;
}
// Stop animation
stopBounce() {
if (!this.logoElement) return;
this.logoElement.classList.remove('goondex-logo-animated');
this.isAnimating = false;
}
// Auto-start for loading screens
autoStart(duration = 3000) {
this.startBounce();
setTimeout(() => this.stopBounce(), duration);
}
}
// Export for use in loading screens
window.LogoAnimator = LogoAnimator;

View File

@ -15,7 +15,7 @@
<p class="hero-subtitle">Full-library sync with seamless enrichment</p>
<div class="hero-actions">
<button type="button" class="btn" onclick="bulkImportAll()">
<button type="button" class="btn btn-light-primary" onclick="bulkImportAll()">
Full Import
<div class="hoverEffect"><div></div></div>
</button>
@ -56,7 +56,7 @@
</div>
<div class="d-grid gap-2">
<button type="button" class="btn w-100" onclick="bulkImportAll()">
<button type="button" class="btn btn-light-primary w-100" onclick="bulkImportAll()">
Full Import
<div class="hoverEffect"><div></div></div>
</button>

View File

@ -59,6 +59,9 @@
</nav>
<div id="global-loader" class="global-loader" style="display:none;">
<div class="loader-content">
<div class="logo">
<img src="/static/img/logo/GOONDEX_Titty.svg" alt="Goondex" width="90" height="55">
</div>
<div class="spinner"></div>
<div id="global-loader-text">Working...</div>
</div>

View File

@ -106,8 +106,8 @@
{{end}}
{{if .Scene.URL}}
<div class="detail-row">
<span class="label">URL:</span>
<span class="value"><a href="{{.Scene.URL}}" target="_blank">View</a></span>
<span class="label">View / Buy:</span>
<span class="value"><a class="btn-link" href="{{.Scene.URL}}" target="_blank" rel="noopener">Open on TPDB</a></span>
</div>
{{end}}
</div>

View File

@ -0,0 +1,6 @@
{
"tpdb_api_key": "Dn8q3mdZd7mE4OHUqf7k1A3q813i48t7q1418zv87c477738",
"ae_api_key": "",
"stashdb_api_key": "",
"stashdb_endpoint": "https://stashdb.org/graphql"
}

50
scripts/enrich.sh Normal file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Enrichment helper (Adult Empire enricher)
# Usage:
# ./scripts/enrich.sh all
# ./scripts/enrich.sh performers
# ./scripts/enrich.sh scenes
# Optional flags are passed through after the subcommand, e.g.:
# ./scripts/enrich.sh performers --start-id 100 --limit 50
set -euo pipefail
cmd="${1:-}"
shift || true
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
run() {
echo "$*"
if [[ -x "$repo_root/goondex" ]]; then
exec "$repo_root/goondex" "$@"
elif [[ -x "$repo_root/bin/goondex" ]]; then
exec "$repo_root/bin/goondex" "$@"
else
echo "goondex binary not found. Build it first with: go build -o bin/goondex ./cmd/goondex" >&2
exit 1
fi
}
case "$cmd" in
all)
run enrich all-performers "$@"
;;
performers|performer)
run enrich all-performers "$@"
;;
scenes|scene)
run enrich all-scenes "$@"
;;
*)
cat <<'EOF' >&2
Usage: ./scripts/enrich.sh {all|performers|scenes} [flags]
Examples:
./scripts/enrich.sh all
./scripts/enrich.sh performers --start-id 100 --limit 50
./scripts/enrich.sh scenes --start-id 200
EOF
exit 1
;;
esac

View File

@ -6,6 +6,16 @@ source "$ROOT/scripts/env.sh"
ADDR="${ADDR:-localhost:8788}"
# Auto-stop if already running on the same port
if command -v lsof >/dev/null 2>&1; then
pids=$(lsof -t -i "@${ADDR#*:}:${ADDR##*:}" 2>/dev/null)
if [[ -n "$pids" ]]; then
echo "Stopping existing goondex on $ADDR (pids: $pids)"
kill $pids 2>/dev/null || true
sleep 0.5
fi
fi
# Build if missing
if [[ ! -x "$ROOT/bin/goondex" ]]; then
echo "Binary not found; building first..."

48
scripts/set_api_key.sh Executable file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Persist TPDB (and optional AE/Stash) API keys to config/api_keys.json
# Usage:
# ./scripts/set_api_key.sh <tpdb-key> [ae-key] [stashdb-key]
#
# This writes config/api_keys.json (gitignored) and echoes an export line
# you can paste to set the env var for the current shell if desired.
set -euo pipefail
tpdb="${1:-}"
ae="${2:-}"
stash="${3:-}"
if [[ -z "$tpdb" ]]; then
echo "Usage: $0 <tpdb-key> [ae-key] [stashdb-key]" >&2
exit 1
fi
python - <<'PY' "$tpdb" "$ae" "$stash"
import json, sys, os
tpdb, ae, stash = sys.argv[1], sys.argv[2] or None, sys.argv[3] or None
path = os.path.join("config", "api_keys.json")
data = {}
if os.path.exists(path):
try:
with open(path, "r") as f:
data = json.load(f)
except Exception:
data = {}
data["tpdb_api_key"] = tpdb
if ae:
data["ae_api_key"] = ae
if stash:
data["stashdb_api_key"] = stash
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
json.dump(data, f, indent=2)
print(f"Wrote {path}")
print(f'TPDB key set: {tpdb[:4]}... (hidden)')
PY
echo "To set the env var for this shell, run:"
echo " export TPDB_API_KEY=\"${tpdb}\""

66
scripts/status.sh Executable file
View File

@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Goondex status snapshot
# Usage: ./scripts/status.sh
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"
# Check binary
if [[ -x "$repo_root/goondex" ]]; then
bin="$repo_root/goondex"
elif [[ -x "$repo_root/bin/goondex" ]]; then
bin="$repo_root/bin/goondex"
else
bin=""
fi
# DB info (file size)
db_path="$repo_root/goondex.db"
db_size="missing"
if [[ -f "$db_path" ]]; then
db_size=$(du -h "$db_path" | awk '{print $1}')
fi
# API key presence
keys_file="$repo_root/config/api_keys.json"
tpdb_key="missing"
if [[ -f "$keys_file" ]]; then
tpdb_key=$(python - <<'PY' "$keys_file"
import json,sys
try:
with open(sys.argv[1]) as f:
data=json.load(f)
key=data.get("tpdb_api_key")
print("set" if key else "missing")
except Exception:
print("missing")
PY
)
fi
# Basic counts (if sqlite3 is available)
scene_count="n/a"; performer_count="n/a"; studio_count="n/a"; movie_count="n/a"
if command -v sqlite3 >/dev/null 2>&1 && [[ -f "$db_path" ]]; then
scene_count=$(sqlite3 "$db_path" 'select count(*) from scenes;') || scene_count="err"
performer_count=$(sqlite3 "$db_path" 'select count(*) from performers;') || performer_count="err"
studio_count=$(sqlite3 "$db_path" 'select count(*) from studios;') || studio_count="err"
movie_count=$(sqlite3 "$db_path" 'select count(*) from movies;') || movie_count="err"
fi
# Status summary
cat <<EOF
Goondex Status
--------------
Repo: $repo_root
Binary: ${bin:-"not built"}
DB: $db_path (${db_size})
Counts: performers=$performer_count, studios=$studio_count, scenes=$scene_count, movies=$movie_count
Keys: TPDB=${tpdb_key}
EOF
# Optional: git status (concise)
if command -v git >/dev/null 2>&1; then
echo "Git:" $(git status --porcelain | wc -l) "dirty file(s)"
fi

75
scripts/tpdb_import.sh Executable file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env bash
# TPDB import helper (TUI-friendly runner)
# Usage:
# ./scripts/tpdb_import.sh all
# ./scripts/tpdb_import.sh performers
# ./scripts/tpdb_import.sh studios
# ./scripts/tpdb_import.sh scenes
set -euo pipefail
cmd="${1:-}"
# Try env first, then config/api_keys.json
if [[ -z "${TPDB_API_KEY:-}" ]]; then
if [[ -f "../config/api_keys.json" ]]; then
TPDB_API_KEY="$(
python - <<'PY' "../config/api_keys.json"
import json, sys
p = sys.argv[1]
try:
with open(p) as f:
data = json.load(f)
print(data.get("tpdb_api_key", ""))
except Exception:
print("")
PY
)"
fi
fi
if [[ -z "${TPDB_API_KEY:-}" ]]; then
echo "TPDB_API_KEY is not set. Export it, or save it via scripts/set_api_key.sh." >&2
echo ' export TPDB_API_KEY="your-key-here"' >&2
exit 1
fi
run() {
echo "$*"
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if [[ -x "$repo_root/goondex" ]]; then
exec "$repo_root/goondex" "$@"
elif [[ -x "$repo_root/bin/goondex" ]]; then
exec "$repo_root/bin/goondex" "$@"
else
echo "goondex binary not found. Build it first with: go build -o bin/goondex ./cmd/goondex" >&2
exit 1
fi
}
case "$cmd" in
all)
run import all
;;
performers|performer)
run import performer
;;
studios|studio)
run import studio
;;
scenes|scene)
run import scene
;;
*)
cat <<'EOF' >&2
Usage: ./scripts/tpdb_import.sh {all|performers|studios|scenes}
Examples:
./scripts/tpdb_import.sh all
./scripts/tpdb_import.sh performers
./scripts/tpdb_import.sh studios
./scripts/tpdb_import.sh scenes
EOF
exit 1
;;
esac

310
test-logo-standalone.html Normal file
View File

@ -0,0 +1,310 @@
<!DOCTYPE html>
<html>
<head>
<title>Logo Animation Test</title>
<style>
body { background: #1a1a1a; color: white; padding: 2rem; font-family: Arial, sans-serif; }
.logo { margin: 2rem 0; width: 180px; height: 110px; }
.logo svg { width: 100%; height: 100%; display: block; }
.goondex-logo-animated .breast-left,
.goondex-logo-animated .breast-right {
animation: breastBounce 1.6s ease-in-out infinite;
transform-origin: center center;
}
.goondex-logo-animated .breast-right { animation-delay: 0.08s; }
.goondex-logo-animated .nipple-left,
.goondex-logo-animated .nipple-right {
animation: nippleBob 1.6s ease-in-out infinite;
transform-origin: center center;
}
.goondex-logo-animated .nipple-right { animation-delay: 0.12s; }
@keyframes breastBounce {
0% { transform: translateY(0) scale(1); }
12% { transform: translateY(-4px) scaleX(1.01) scaleY(0.985); }
28% { transform: translateY(8px) scaleX(0.99) scaleY(1.03); }
44% { transform: translateY(-3px) scaleX(1.012) scaleY(0.988); }
60% { transform: translateY(4px) scaleX(0.995) scaleY(1.015); }
100% { transform: translateY(0) scale(1); }
}
@keyframes nippleBob {
0%, 100% { transform: translate(0, 0); }
18% { transform: translate(0px, -5px) scale(1.03); }
35% { transform: translate(0px, 6px) scale(0.98); }
55% { transform: translate(0px, -3px) scale(1.02); }
75% { transform: translate(0px, 2px); }
}
button { background: #ff5fa2; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; margin-right: 1rem; cursor: pointer; }
.global-loader {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(2px);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.global-loader .loader-content {
background: #2a2a2a;
padding: 1.5rem 2rem;
border-radius: 12px;
border: 1px solid #444;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
color: white;
min-width: 280px;
justify-content: center;
}
.global-loader .logo svg {
width: 90px;
height: 55px;
filter: drop-shadow(0 2px 8px rgba(255, 95, 162, 0.3));
}
</style>
</head>
<body>
<h1>Goondex Logo Animation Test</h1>
<div style="margin: 2rem 0;">
<h2>Static Logo:</h2>
<div id="static-logo" class="logo"></div>
</div>
<div style="margin: 2rem 0;">
<h2>Animated Logo:</h2>
<div id="animated-logo" class="logo"></div>
</div>
<div style="margin: 2rem 0;">
<button onclick="startAnimation()">Start Animation</button>
<button onclick="stopAnimation()">Stop Animation</button>
</div>
<div style="margin: 2rem 0;">
<button onclick="testLoader()">Test Loader (3 seconds)</button>
</div>
<div id="global-loader" class="global-loader" style="display:none;">
<div class="loader-content">
<div id="loader-logo" class="logo"></div>
<div>Working...</div>
</div>
</div>
<script>
class LogoAnimator {
constructor() {
this.isAnimating = false;
this.logoElement = null;
}
init(svgElement) {
this.logoElement = svgElement;
this.identifyParts();
}
identifyParts() {
if (!this.logoElement) return;
const nipples = [];
const breasts = [];
// Prefer elements with ids/classes if present
const breastCandidates = [
this.logoElement.querySelector('#breast-left'),
this.logoElement.querySelector('#breast-right')
].filter(Boolean);
const nippleCandidates = [
this.logoElement.querySelector('#nipple-left'),
this.logoElement.querySelector('#nipple-right')
].filter(Boolean);
breasts.push(...breastCandidates);
nipples.push(...nippleCandidates);
// Fallback nipples: first two circles/ellipses
if (nipples.length < 2) {
const circ = Array.from(this.logoElement.querySelectorAll('circle, ellipse'));
while (nipples.length < 2 && circ.length) {
nipples.push(circ.shift());
}
}
// Fallback breasts: first two paths/shapes
if (breasts.length < 2) {
const shapes = Array.from(this.logoElement.querySelectorAll('path, polygon, rect'));
while (breasts.length < 2 && shapes.length) {
breasts.push(shapes.shift());
}
}
// Ultimate fallback: animate whole svg as a single breast pair
if (breasts.length === 0) breasts.push(this.logoElement);
if (breasts.length === 1) breasts.push(this.logoElement);
if (breasts[0]) breasts[0].classList.add('breast-left');
if (breasts[1]) breasts[1].classList.add('breast-right');
if (nipples.length === 0) {
// If no explicit nipples, piggyback on breasts so some motion happens
nipples.push(breasts[0], breasts[1]);
}
nipples.slice(0, 2).forEach((el, idx) => {
if (el) el.classList.add(idx === 0 ? 'nipple-left' : 'nipple-right');
});
}
startBounce() {
if (!this.logoElement || this.isAnimating) return;
this.logoElement.classList.add('goondex-logo-animated');
this.isAnimating = true;
}
stopBounce() {
if (!this.logoElement) return;
this.logoElement.classList.remove('goondex-logo-animated');
this.isAnimating = false;
}
}
// Inline-load the SVG so we can animate internals
// INLINE ANIMATOR (self-contained for this test page)
class LogoAnimator {
constructor() {
this.isAnimating = false;
this.logoElement = null;
}
init(svgElement) {
this.logoElement = svgElement;
this.identifyParts();
}
identifyParts() {
if (!this.logoElement) return;
const nipples = [];
const breasts = [];
const breastCandidates = [
this.logoElement.querySelector('#breast-left'),
this.logoElement.querySelector('#breast-right')
].filter(Boolean);
const nippleCandidates = [
this.logoElement.querySelector('#nipple-left'),
this.logoElement.querySelector('#nipple-right')
].filter(Boolean);
breasts.push(...breastCandidates);
nipples.push(...nippleCandidates);
if (nipples.length < 2) {
const circ = Array.from(this.logoElement.querySelectorAll('circle, ellipse'));
while (nipples.length < 2 && circ.length) nipples.push(circ.shift());
}
if (breasts.length < 2) {
const shapes = Array.from(this.logoElement.querySelectorAll('path, polygon, rect'));
while (breasts.length < 2 && shapes.length) breasts.push(shapes.shift());
}
if (breasts.length === 0) breasts.push(this.logoElement);
if (breasts.length === 1) breasts.push(this.logoElement);
if (breasts[0]) breasts[0].classList.add('breast-left');
if (breasts[1]) breasts[1].classList.add('breast-right');
if (nipples.length === 0) nipples.push(breasts[0], breasts[1]);
nipples.slice(0, 2).forEach((el, idx) => el && el.classList.add(idx === 0 ? 'nipple-left' : 'nipple-right'));
}
startBounce() {
if (!this.logoElement || this.isAnimating) return;
this.logoElement.classList.add('goondex-logo-animated');
this.isAnimating = true;
}
stopBounce() {
if (!this.logoElement) return;
this.logoElement.classList.remove('goondex-logo-animated');
this.isAnimating = false;
}
}
async function loadSVG(urls, targetId) {
const target = document.getElementById(targetId);
if (!target) return null;
for (const url of urls) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error('fetch failed');
const svgText = await res.text();
target.innerHTML = svgText;
const svg = target.querySelector('svg');
return svg;
} catch (e) {
continue;
}
}
// Fallback to img if all fetches fail (no animation possible)
target.innerHTML = `<img src=\"${urls[0]}\" alt=\"Goondex Logo\" width=\"100%\" height=\"100%\">`;
return null;
}
const logoURLs = [
"/static/img/logo/GOONDEX_Titty.svg",
"static/img/logo/GOONDEX_Titty.svg",
"./static/img/logo/GOONDEX_Titty.svg"
];
let animator = null;
let loaderAnimator = null;
async function initLogos() {
const staticSvg = await loadSVG(logoURLs, 'static-logo');
const animatedSvg = await loadSVG(logoURLs, 'animated-logo');
const loaderSvg = await loadSVG(logoURLs, 'loader-logo');
if (animatedSvg) {
animator = new LogoAnimator();
animator.init(animatedSvg);
animator.startBounce();
}
if (loaderSvg) {
loaderAnimator = new LogoAnimator();
loaderAnimator.init(loaderSvg);
}
}
function startAnimation() {
if (animator) animator.startBounce();
}
function stopAnimation() {
if (animator) animator.stopBounce();
}
function testLoader() {
const loader = document.getElementById('global-loader');
loader.style.display = 'flex';
if (loaderAnimator) {
loaderAnimator.startBounce();
}
setTimeout(() => {
loader.style.display = 'none';
if (loaderAnimator) {
loaderAnimator.stopBounce();
}
}, 3000);
}
initLogos();
</script>
</body>
</html>

64
test-logo.html Normal file
View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<title>Logo Animation Test</title>
<link rel="stylesheet" href="/static/css/goondex.css">
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/logo-animation.css">
</head>
<body style="background: #1a1a1a; color: white; padding: 2rem;">
<h1>Goondex Logo Animation Test</h1>
<div style="margin: 2rem 0;">
<h2>Static Logo:</h2>
<div class="logo">
<img src="/static/img/logo/GOONDEX_Titty.svg" alt="Goondex" width="180" height="110">
</div>
</div>
<div style="margin: 2rem 0;">
<h2>Animated Logo:</h2>
<div class="logo">
<img id="animated-logo" src="/static/img/logo/GOONDEX_Titty.svg" alt="Goondex" width="180" height="110">
</div>
</div>
<div style="margin: 2rem 0;">
<button onclick="startAnimation()" style="background: #ff5fa2; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; margin-right: 1rem;">Start Animation</button>
<button onclick="stopAnimation()" style="background: #666; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px;">Stop Animation</button>
</div>
<div style="margin: 2rem 0;">
<h2>Loader Test:</h2>
<button onclick="testLoader()" style="background: #ff5fa2; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px;">Test Loader (3 seconds)</button>
</div>
<script src="/static/js/logo-animation.js"></script>
<script src="/static/js/app.js"></script>
<script>
let animator = null;
function startAnimation() {
const logo = document.getElementById('animated-logo');
if (!animator) {
animator = new LogoAnimator();
animator.init(logo);
}
animator.startBounce();
}
function stopAnimation() {
if (animator) {
animator.stopBounce();
}
}
function testLoader() {
showLoader('Testing logo animation in loader...');
setTimeout(() => {
hideLoader();
}, 3000);
}
</script>
</body>
</html>