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:
parent
073fc49745
commit
2b4a2038fa
|
|
@ -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
|
||||
|
||||
|
|
|
|||
74
docs/JAV_STUDIOS_REFERENCE.md
Normal file
74
docs/JAV_STUDIOS_REFERENCE.md
Normal 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.
|
||||
|
|
@ -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`).
|
||||
|
|
|
|||
|
|
@ -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++
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
28
internal/web/static/css/logo-animation.css
Normal file
28
internal/web/static/css/logo-animation.css
Normal 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); }
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -7,11 +7,42 @@ function openModal(modalId) {
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
103
internal/web/static/js/logo-anim.js
Normal file
103
internal/web/static/js/logo-anim.js
Normal 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;
|
||||
}
|
||||
})();
|
||||
58
internal/web/static/js/logo-animation.js
Normal file
58
internal/web/static/js/logo-animation.js
Normal 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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
6
scripts/config/api_keys.json
Normal file
6
scripts/config/api_keys.json
Normal 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
50
scripts/enrich.sh
Normal 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
|
||||
|
|
@ -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
48
scripts/set_api_key.sh
Executable 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
66
scripts/status.sh
Executable 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
75
scripts/tpdb_import.sh
Executable 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
310
test-logo-standalone.html
Normal 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
64
test-logo.html
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user