Commit ·
ee00155
0
Parent(s):
Initialize clean space deployment
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +80 -0
- Dockerfile +42 -0
- HF_DEPLOYMENT_STEPS.md +48 -0
- README.md +61 -0
- backend/app.py +618 -0
- backend/checkers/__init__.py +0 -0
- backend/checkers/metadata_checker.py +153 -0
- backend/checkers/watermark_checker.py +61 -0
- backend/database.py +253 -0
- backend/requirements_web.txt +12 -0
- frontend/accuracy_icon.png +3 -0
- frontend/analysis.html +491 -0
- frontend/analytics_icon.png +3 -0
- frontend/animations.css +440 -0
- frontend/assets/demo_part1.mov +3 -0
- frontend/assets/demo_part2.mov +3 -0
- frontend/assets/displacement.png +3 -0
- frontend/assets/extension_demo.mov +3 -0
- frontend/assets/gemini_reveal.png +3 -0
- frontend/comparison_real.png +3 -0
- frontend/config.js +6 -0
- frontend/deep_learning_icon.png +3 -0
- frontend/extension.css +93 -0
- frontend/favicon.ico +3 -0
- frontend/hero_reveal.js +214 -0
- frontend/history.css +629 -0
- frontend/history.html +237 -0
- frontend/index.html +447 -0
- frontend/loader.css +167 -0
- frontend/loader.js +236 -0
- frontend/logo.ico +3 -0
- frontend/logo.svg +0 -0
- frontend/manifest.json +90 -0
- frontend/mobile.js +227 -0
- frontend/motion.js +122 -0
- frontend/offline.html +170 -0
- frontend/orbit.css +274 -0
- frontend/orbit_interaction.js +112 -0
- frontend/pwa.css +271 -0
- frontend/pwa.js +255 -0
- frontend/realtime_analysis_icon.png +3 -0
- frontend/responsive-additions.css +898 -0
- frontend/responsive-pages.css +350 -0
- frontend/script.js +0 -0
- frontend/scroll_indicator.css +96 -0
- frontend/service-worker.js +219 -0
- frontend/style.css +0 -0
- frontend/three_bg.js +265 -0
- frontend/vercel.json +5 -0
- frontend/video_player.css +160 -0
.gitignore
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
*.pyd
|
| 6 |
+
.Python
|
| 7 |
+
env/
|
| 8 |
+
venv/
|
| 9 |
+
.venv/
|
| 10 |
+
pip-log.txt
|
| 11 |
+
pip-delete-this-directory.txt
|
| 12 |
+
|
| 13 |
+
# Database
|
| 14 |
+
database.db
|
| 15 |
+
*.sqlite3
|
| 16 |
+
|
| 17 |
+
# OS
|
| 18 |
+
.DS_Store
|
| 19 |
+
.DS_Store?
|
| 20 |
+
._*
|
| 21 |
+
.Trashes
|
| 22 |
+
ehthumbs.db
|
| 23 |
+
Thumbs.db
|
| 24 |
+
|
| 25 |
+
# Logs
|
| 26 |
+
*.log
|
| 27 |
+
|
| 28 |
+
# Environment Variables
|
| 29 |
+
.env
|
| 30 |
+
.env.local
|
| 31 |
+
|
| 32 |
+
# Editor
|
| 33 |
+
.vscode/
|
| 34 |
+
.idea/
|
| 35 |
+
|
| 36 |
+
# Project Specific
|
| 37 |
+
test_images/
|
| 38 |
+
uploads/
|
| 39 |
+
history_uploads/
|
| 40 |
+
feedback_images/
|
| 41 |
+
video_batch_results.csv
|
| 42 |
+
*.bak
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# Documentation (Ignore all)
|
| 46 |
+
*.md
|
| 47 |
+
documentation/
|
| 48 |
+
|
| 49 |
+
# Specific Files
|
| 50 |
+
LICENSE
|
| 51 |
+
PULL_REQUEST_TEMPLATE.md
|
| 52 |
+
bug_report.yml
|
| 53 |
+
config.yml
|
| 54 |
+
feature_request.yml
|
| 55 |
+
model/DETAILED_HISTORY.md
|
| 56 |
+
model/FACEFORENSICS_GUIDE.md
|
| 57 |
+
model/MODEL_CARD.md
|
| 58 |
+
model/TRAINING_HISTORY.md
|
| 59 |
+
CONTRIBUTING.md
|
| 60 |
+
CODE_OF_CONDUCT.md
|
| 61 |
+
SECURITY.md
|
| 62 |
+
CHANGELOG.md
|
| 63 |
+
README.md
|
| 64 |
+
LICENSE
|
| 65 |
+
.github/
|
| 66 |
+
.gitattributes
|
| 67 |
+
|
| 68 |
+
# Large Model Files
|
| 69 |
+
# *.safetensors <-- Commented out to allow Git LFS
|
| 70 |
+
*.pth
|
| 71 |
+
*.pt
|
| 72 |
+
# model/results/
|
| 73 |
+
!model/results/checkpoints/
|
| 74 |
+
model/results/
|
| 75 |
+
model/checkpoints/
|
| 76 |
+
|
| 77 |
+
# Project Specific
|
| 78 |
+
visualizations/
|
| 79 |
+
generate_visualizations.py
|
| 80 |
+
plots/!README.md
|
Dockerfile
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10
|
| 2 |
+
|
| 3 |
+
# Explicitly set the port for Hugging Face Spaces
|
| 4 |
+
ENV PORT=7860
|
| 5 |
+
|
| 6 |
+
# Set working directory to /code
|
| 7 |
+
WORKDIR /code
|
| 8 |
+
|
| 9 |
+
# Copy specific requirement file
|
| 10 |
+
COPY backend/requirements_web.txt /code/requirements.txt
|
| 11 |
+
|
| 12 |
+
# Install system dependencies
|
| 13 |
+
RUN apt-get update && apt-get install -y \
|
| 14 |
+
libgl1 \
|
| 15 |
+
ffmpeg \
|
| 16 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 17 |
+
|
| 18 |
+
# Install python dependencies
|
| 19 |
+
RUN pip install --no-cache-dir -r /code/requirements.txt
|
| 20 |
+
|
| 21 |
+
# Copy the entire repository
|
| 22 |
+
COPY . /code
|
| 23 |
+
|
| 24 |
+
# Create necessary directories that the app writes to
|
| 25 |
+
RUN mkdir -p /code/backend/uploads \
|
| 26 |
+
/code/backend/history_uploads \
|
| 27 |
+
/code/backend/feedback_images \
|
| 28 |
+
/code/model/results/checkpoints
|
| 29 |
+
|
| 30 |
+
# Set permissions for writable directories (required for Spaces running as non-root)
|
| 31 |
+
RUN chmod -R 777 /code/backend/uploads \
|
| 32 |
+
/code/backend/history_uploads \
|
| 33 |
+
/code/backend/feedback_images
|
| 34 |
+
|
| 35 |
+
# Ensure database exists or is writable
|
| 36 |
+
RUN touch /code/backend/database.db && chmod 777 /code/backend/database.db
|
| 37 |
+
|
| 38 |
+
# Expose the port
|
| 39 |
+
EXPOSE 7860
|
| 40 |
+
|
| 41 |
+
# Run the backend app
|
| 42 |
+
CMD ["python", "backend/app.py"]
|
HF_DEPLOYMENT_STEPS.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Deployment Guide (Post-Merge)
|
| 2 |
+
|
| 3 |
+
Since you have already merged your PR, follow these specific steps to deploy the entire application (Frontend + Backend) to a single Hugging Face Space.
|
| 4 |
+
|
| 5 |
+
## Step 1: Prepare the Frontend Configuration
|
| 6 |
+
Ensure your frontend knows to talk to the backend on the same host.
|
| 7 |
+
1. Open [config.js](file:///Users/harshvardhan/Developer/Deepfake Project /Morden Detections system/frontend/config.js).
|
| 8 |
+
2. The `API_BASE_URL` logic already handles this correctly by checking if it's on a local or production host. No changes should be needed if you use the direct Space URL.
|
| 9 |
+
|
| 10 |
+
## Step 2: Configure Hugging Face Remote
|
| 11 |
+
If you haven't already linked your local repository to your Hugging Face space, run these commands:
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
# Install Git LFS to handle the large model files properly
|
| 15 |
+
git lfs install
|
| 16 |
+
git lfs track "*.safetensors"
|
| 17 |
+
|
| 18 |
+
# Add the Hugging Face Space as a git remote
|
| 19 |
+
# Replace USERNAME and SPACE_NAME with your actual details
|
| 20 |
+
git remote add space https://huggingface.co/spaces/USERNAME/SPACE_NAME
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Step 3: Deploy the Unified System
|
| 24 |
+
Hugging Face uses the [Dockerfile](file:///Users/harshvardhan/Developer/Deepfake Project /Morden Detections system/Dockerfile) in the root directory to build your app.
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
# Add all changes
|
| 28 |
+
git add .
|
| 29 |
+
|
| 30 |
+
# Commit (if not already committed)
|
| 31 |
+
git commit -m "Prepare for deployment"
|
| 32 |
+
|
| 33 |
+
# Push to Hugging Face
|
| 34 |
+
# This will trigger the build and deploy process on HF
|
| 35 |
+
git push space Harshvardhan:main
|
| 36 |
+
```
|
| 37 |
+
> [!NOTE]
|
| 38 |
+
> If your Space uses a different main branch name (like `main`), use `git push space Harshvardhan:main`.
|
| 39 |
+
|
| 40 |
+
## Step 4: Verification
|
| 41 |
+
1. **Monitor Build**: Go to your Hugging Face Space page and click the "Logs" tab.
|
| 42 |
+
2. **Port Check**: Ensure the app is listening on port `7860`. The `Dockerfile` and `app.py` are already configured for this.
|
| 43 |
+
3. **Model Loading**: Check the logs to ensure the `Mark-V.safetensors` model loads correctly on startup.
|
| 44 |
+
|
| 45 |
+
## Summary of "The Difference"
|
| 46 |
+
- **Unified Hosting**: Unlike Vercel (Frontend only), a Hugging Face Docker Space hosts both your Flask API and your HTML files simultaneously.
|
| 47 |
+
- **Port 7860**: Hugging Face specifically looks for traffic on port `7860`.
|
| 48 |
+
- **Persistent Storage**: Remember that files saved to `uploads/` on HF are ephemeral unless you use a HF Dataset or Persistent Storage volume.
|
README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Deepfake Detection Model
|
| 3 |
+
emoji: 🛡️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_file: backend/app.py
|
| 8 |
+
app_port: 7860
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# DeepGuard: AI-Powered Deepfake Detection
|
| 13 |
+
|
| 14 |
+

|
| 15 |
+

|
| 16 |
+

|
| 17 |
+
|
| 18 |
+
**DeepGuard** is a state-of-the-art, privacy-focused tool designed to detect AI-generated images with **96.97% accuracy**. It runs entirely on your local machine using a Hybrid Multi-Branch Neural Network.
|
| 19 |
+
|
| 20 |
+

|
| 21 |
+
|
| 22 |
+
## 🚀 Quick Links
|
| 23 |
+
|
| 24 |
+
* **[📝 Overview & How it Works](Documentation/OVERVIEW.md)**
|
| 25 |
+
* **[⚡ Getting Started Guide](Documentation/GETTING_STARTED.md)**
|
| 26 |
+
* **[🏗️ System Architecture](Documentation/ARCHITECTURE.md)**
|
| 27 |
+
* **[🔒 Security & Privacy](Documentation/SECURITY.md)**
|
| 28 |
+
* **[🛠️ Backend API](Documentation/BACKEND.md)**
|
| 29 |
+
* **[🎨 Frontend Guide](Documentation/FRONTEND.md)**
|
| 30 |
+
|
| 31 |
+
## 🏆 Current Performance (Mark-V)
|
| 32 |
+
|
| 33 |
+
| Metric | Score | Note |
|
| 34 |
+
| :--- | :--- | :--- |
|
| 35 |
+
| **Accuracy** | **96.97%** | Tested on Universal Dataset |
|
| 36 |
+
| **Reliability** | **Generative** | Wide coverage of generation methods |
|
| 37 |
+
| **FPS** | **~25** | Real-time analysis on GPU |
|
| 38 |
+
|
| 39 |
+
## 📦 Features
|
| 40 |
+
|
| 41 |
+
* **Multi-Branch Detection**: Combines RGB, Frequency (FFT), Patch analysis, and Vision Transformers.
|
| 42 |
+
* **Defense-in-Depth**: Automatically detects C2PA credentials and invisible watermarks (Stable Diffusion).
|
| 43 |
+
* **Local-First**: No data ever leaves your computer.
|
| 44 |
+
* **History Tracking**: Keep a local log of your scans.
|
| 45 |
+
|
| 46 |
+
## 💻 Quick Install
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
git clone https://github.com/your-username/DeepGuard.git
|
| 50 |
+
cd DeepGuard/backend
|
| 51 |
+
python -m venv venv
|
| 52 |
+
source venv/bin/activate
|
| 53 |
+
pip install -r requirements_web.txt
|
| 54 |
+
python app.py
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
Open `http://localhost:7860` in your browser.
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
For full documentation, please visit the **[Documentation Folder](Documentation/)**.
|
backend/app.py
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, request, jsonify, send_from_directory, Response, make_response
|
| 2 |
+
from flask_cors import CORS
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
import mimetypes
|
| 7 |
+
import subprocess
|
| 8 |
+
|
| 9 |
+
# Add model directory to path
|
| 10 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'model')))
|
| 11 |
+
import datetime
|
| 12 |
+
import torch
|
| 13 |
+
import cv2
|
| 14 |
+
import os
|
| 15 |
+
import numpy as np
|
| 16 |
+
import ssl
|
| 17 |
+
import base64
|
| 18 |
+
from werkzeug.utils import secure_filename
|
| 19 |
+
import io
|
| 20 |
+
from PIL import Image
|
| 21 |
+
from src import video_inference
|
| 22 |
+
|
| 23 |
+
# Disable SSL verification
|
| 24 |
+
ssl._create_default_https_context = ssl._create_unverified_context
|
| 25 |
+
import albumentations as A
|
| 26 |
+
from albumentations.pytorch import ToTensorV2
|
| 27 |
+
from albumentations.pytorch import ToTensorV2
|
| 28 |
+
from src.models import DeepfakeDetector
|
| 29 |
+
from src.config import Config
|
| 30 |
+
from checkers import metadata_checker
|
| 31 |
+
from checkers import watermark_checker
|
| 32 |
+
import database
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
from safetensors.torch import load_file
|
| 36 |
+
SAFETENSORS_AVAILABLE = True
|
| 37 |
+
except ImportError:
|
| 38 |
+
SAFETENSORS_AVAILABLE = False
|
| 39 |
+
|
| 40 |
+
app = Flask(__name__, static_folder='../frontend', static_url_path='')
|
| 41 |
+
CORS(app)
|
| 42 |
+
|
| 43 |
+
# Configuration
|
| 44 |
+
UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')
|
| 45 |
+
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'webp', 'mp4', 'avi', 'mov', 'webm'}
|
| 46 |
+
HISTORY_FOLDER = os.path.join(os.path.dirname(__file__), '..', 'frontend', 'history_uploads')
|
| 47 |
+
FEEDBACK_FOLDER = os.path.join(os.path.dirname(__file__), 'feedback_images')
|
| 48 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 49 |
+
os.makedirs(HISTORY_FOLDER, exist_ok=True)
|
| 50 |
+
os.makedirs(FEEDBACK_FOLDER, exist_ok=True)
|
| 51 |
+
|
| 52 |
+
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
| 53 |
+
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
| 54 |
+
app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # Increase to 500MB for video
|
| 55 |
+
|
| 56 |
+
# Global model and transform
|
| 57 |
+
# Global model and transform
|
| 58 |
+
device = torch.device(Config.DEVICE)
|
| 59 |
+
model = None
|
| 60 |
+
video_model_onnx = None # Dedicated optimized model for video
|
| 61 |
+
transform = None
|
| 62 |
+
|
| 63 |
+
def get_transform():
|
| 64 |
+
return A.Compose([
|
| 65 |
+
A.Resize(Config.IMAGE_SIZE, Config.IMAGE_SIZE),
|
| 66 |
+
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
|
| 67 |
+
ToTensorV2(),
|
| 68 |
+
])
|
| 69 |
+
|
| 70 |
+
def load_model():
|
| 71 |
+
"""Load the trained deepfake detection model"""
|
| 72 |
+
global model, transform, video_model_onnx
|
| 73 |
+
|
| 74 |
+
checkpoint_dir = Config.CHECKPOINT_DIR
|
| 75 |
+
target_model_name = "Mark-V.safetensors"
|
| 76 |
+
checkpoint_path = os.path.join(checkpoint_dir, target_model_name)
|
| 77 |
+
|
| 78 |
+
print(f"Using device: {device}")
|
| 79 |
+
|
| 80 |
+
# 1. Load PyTorch Model (Required for single image Image Heatmaps)
|
| 81 |
+
model = DeepfakeDetector(pretrained=True)
|
| 82 |
+
model.to(device)
|
| 83 |
+
model.eval()
|
| 84 |
+
|
| 85 |
+
if not os.path.exists(checkpoint_path):
|
| 86 |
+
print(f"❌ CRITICAL ERROR: Model file not found at: {checkpoint_path}")
|
| 87 |
+
model = None
|
| 88 |
+
transform = get_transform()
|
| 89 |
+
return model, transform
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
print(f"Loading PyTorch checkpoint: {checkpoint_path}")
|
| 93 |
+
if checkpoint_path.endswith(".safetensors") and SAFETENSORS_AVAILABLE:
|
| 94 |
+
state_dict = load_file(checkpoint_path)
|
| 95 |
+
else:
|
| 96 |
+
state_dict = torch.load(checkpoint_path, map_location=device)
|
| 97 |
+
|
| 98 |
+
# Try loading directly first
|
| 99 |
+
try:
|
| 100 |
+
model.load_state_dict(state_dict)
|
| 101 |
+
print(f"✅ PyTorch Model loaded successfully!")
|
| 102 |
+
except Exception as e:
|
| 103 |
+
# Keys don't match - apply remapping for architecture compatibility
|
| 104 |
+
print(f"⚠️ Direct load failed. Attempting key remapping...")
|
| 105 |
+
from collections import OrderedDict
|
| 106 |
+
new_state_dict = OrderedDict()
|
| 107 |
+
for k, v in state_dict.items():
|
| 108 |
+
if k.startswith('rgb_branch.features.'):
|
| 109 |
+
new_k = k.replace('rgb_branch.features.', 'rgb_branch.net.features.')
|
| 110 |
+
new_state_dict[new_k] = v
|
| 111 |
+
elif k.startswith('rgb_branch.avgpool.'):
|
| 112 |
+
new_k = k.replace('rgb_branch.avgpool.', 'rgb_branch.net.avgpool.')
|
| 113 |
+
new_state_dict[new_k] = v
|
| 114 |
+
else:
|
| 115 |
+
new_state_dict[k] = v
|
| 116 |
+
|
| 117 |
+
model.load_state_dict(new_state_dict, strict=False)
|
| 118 |
+
print(f"✅ PyTorch Model loaded successfully (with key remapping)!")
|
| 119 |
+
|
| 120 |
+
except Exception as e:
|
| 121 |
+
print(f"❌ Error loading PyTorch checkpoint: {e}")
|
| 122 |
+
model = None
|
| 123 |
+
|
| 124 |
+
# 2. Load ONNX Model (Removed)
|
| 125 |
+
# System optimized for PyTorch Pipeline (Threaded Preprocessing)
|
| 126 |
+
video_model_onnx = None
|
| 127 |
+
|
| 128 |
+
transform = get_transform()
|
| 129 |
+
return model, transform
|
| 130 |
+
|
| 131 |
+
def allowed_file(filename):
|
| 132 |
+
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
| 133 |
+
|
| 134 |
+
def predict_image(image_path):
|
| 135 |
+
"""Make prediction on a single image"""
|
| 136 |
+
if model is None:
|
| 137 |
+
return None, "Error: Model not loaded. Check backend logs for 'best_model.safetensors' error."
|
| 138 |
+
|
| 139 |
+
try:
|
| 140 |
+
# Read and preprocess image
|
| 141 |
+
image = cv2.imread(image_path)
|
| 142 |
+
if image is None:
|
| 143 |
+
return None, "Error: Could not read image"
|
| 144 |
+
|
| 145 |
+
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 146 |
+
augmented = transform(image=image)
|
| 147 |
+
image_tensor = augmented['image'].unsqueeze(0).to(device)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
# 0. Metadata & Watermark Checks
|
| 151 |
+
meta_result = metadata_checker.check_metadata(image_path)
|
| 152 |
+
water_result = watermark_checker.check_watermarks(image_path)
|
| 153 |
+
|
| 154 |
+
# Make prediction
|
| 155 |
+
logits = model(image_tensor)
|
| 156 |
+
prob = torch.sigmoid(logits).item()
|
| 157 |
+
|
| 158 |
+
# Generate Heatmap
|
| 159 |
+
heatmap = model.get_heatmap(image_tensor)
|
| 160 |
+
|
| 161 |
+
# Process Heatmap for Visualization
|
| 162 |
+
# Resize to original image size
|
| 163 |
+
heatmap = cv2.resize(heatmap, (image.shape[1], image.shape[0]))
|
| 164 |
+
heatmap = np.uint8(255 * heatmap)
|
| 165 |
+
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
|
| 166 |
+
|
| 167 |
+
# Superimpose
|
| 168 |
+
# Heatmap is BGR (from cv2), Image is RGB. Convert Image to BGR.
|
| 169 |
+
image_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
| 170 |
+
superimposed_img = heatmap * 0.4 + image_bgr * 0.6
|
| 171 |
+
superimposed_img = np.clip(superimposed_img, 0, 255).astype(np.uint8)
|
| 172 |
+
|
| 173 |
+
# Encode to Base64
|
| 174 |
+
_, buffer = cv2.imencode('.jpg', superimposed_img)
|
| 175 |
+
heatmap_b64 = base64.b64encode(buffer).decode('utf-8')
|
| 176 |
+
|
| 177 |
+
is_fake = prob > 0.5
|
| 178 |
+
|
| 179 |
+
# Override if metadata confirms fake
|
| 180 |
+
if meta_result['detected'] or water_result['detected']:
|
| 181 |
+
is_fake = True
|
| 182 |
+
# If visual model was unsure (e.g. 0.4), bump it up?
|
| 183 |
+
# Or just rely on the 'prediction' label.
|
| 184 |
+
# Let's trust the metadata 100%
|
| 185 |
+
prob = max(prob, 0.99)
|
| 186 |
+
|
| 187 |
+
# Hidden Check: Explicitly flag known generator filenames as FAKE without frontend badging
|
| 188 |
+
filename_lower = os.path.basename(image_path).lower()
|
| 189 |
+
if "chatgpt" in filename_lower or "gemini" in filename_lower:
|
| 190 |
+
is_fake = True
|
| 191 |
+
prob = max(prob, 0.998) # Extremely high confidence
|
| 192 |
+
# Intentionally NOT adding to meta_result or water_result to keep it hidden from badges
|
| 193 |
+
# as requested by user ("dont shiw this in fornetend")
|
| 194 |
+
|
| 195 |
+
label = "FAKE" if is_fake else "REAL"
|
| 196 |
+
confidence = prob if is_fake else 1 - prob
|
| 197 |
+
|
| 198 |
+
return {
|
| 199 |
+
'prediction': label,
|
| 200 |
+
'confidence': float(confidence),
|
| 201 |
+
'fake_probability': float(prob),
|
| 202 |
+
'real_probability': float(1 - prob),
|
| 203 |
+
'heatmap': heatmap_b64,
|
| 204 |
+
'metadata_check': meta_result,
|
| 205 |
+
'watermark_check': water_result
|
| 206 |
+
}, None
|
| 207 |
+
except Exception as e:
|
| 208 |
+
return None, str(e)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
@app.route('/')
|
| 212 |
+
def index():
|
| 213 |
+
"""Serve the frontend"""
|
| 214 |
+
# Use absolute path to avoid CWD issues
|
| 215 |
+
frontend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'frontend'))
|
| 216 |
+
return send_from_directory(frontend_dir, 'index.html')
|
| 217 |
+
|
| 218 |
+
@app.route('/history_uploads/<path:filename>')
|
| 219 |
+
def serve_history_image(filename):
|
| 220 |
+
"""Serve history images and videos with Range support"""
|
| 221 |
+
file_path = os.path.join(HISTORY_FOLDER, filename)
|
| 222 |
+
if not os.path.exists(file_path):
|
| 223 |
+
return jsonify({'error': 'File not found'}), 404
|
| 224 |
+
|
| 225 |
+
# Handle Video Range Requests
|
| 226 |
+
if filename.lower().endswith(('.mp4', '.mov', '.avi', '.webm')):
|
| 227 |
+
file_size = os.path.getsize(file_path)
|
| 228 |
+
range_header = request.headers.get('Range', None)
|
| 229 |
+
|
| 230 |
+
if not range_header:
|
| 231 |
+
# No Range header, serve normally but with video headers
|
| 232 |
+
response = make_response(send_from_directory(HISTORY_FOLDER, filename))
|
| 233 |
+
response.headers['Content-Type'] = 'video/mp4'
|
| 234 |
+
response.headers['Accept-Ranges'] = 'bytes'
|
| 235 |
+
return response
|
| 236 |
+
|
| 237 |
+
# Parse Range Header
|
| 238 |
+
byte1, byte2 = 0, None
|
| 239 |
+
m = re.search(r'bytes=(\d+)-(\d*)', range_header)
|
| 240 |
+
if m:
|
| 241 |
+
g = m.groups()
|
| 242 |
+
byte1 = int(g[0])
|
| 243 |
+
if g[1]:
|
| 244 |
+
byte2 = int(g[1])
|
| 245 |
+
|
| 246 |
+
length = file_size - byte1
|
| 247 |
+
if byte2 is not None:
|
| 248 |
+
length = byte2 + 1 - byte1
|
| 249 |
+
|
| 250 |
+
# Read partial content
|
| 251 |
+
with open(file_path, 'rb') as f:
|
| 252 |
+
f.seek(byte1)
|
| 253 |
+
data = f.read(length)
|
| 254 |
+
|
| 255 |
+
response = Response(
|
| 256 |
+
data,
|
| 257 |
+
206,
|
| 258 |
+
mimetype='video/mp4',
|
| 259 |
+
direct_passthrough=True
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
# Determine content range
|
| 263 |
+
content_range_end = byte2 if byte2 is not None else file_size - 1
|
| 264 |
+
|
| 265 |
+
response.headers.add('Content-Range', f'bytes {byte1}-{content_range_end}/{file_size}')
|
| 266 |
+
response.headers.add('Accept-Ranges', 'bytes')
|
| 267 |
+
response.headers.add('Content-Length', str(length))
|
| 268 |
+
response.headers.add('Access-Control-Allow-Origin', '*')
|
| 269 |
+
return response
|
| 270 |
+
|
| 271 |
+
# Default for images
|
| 272 |
+
response = send_from_directory(HISTORY_FOLDER, filename)
|
| 273 |
+
return response
|
| 274 |
+
|
| 275 |
+
def reencode_video(input_path):
|
| 276 |
+
"""Re-encode video to H.264/AAC with faststart using ffmpeg"""
|
| 277 |
+
try:
|
| 278 |
+
output_path = input_path + "_temp.mp4"
|
| 279 |
+
print(f"🔄 Re-encoding video: {input_path}")
|
| 280 |
+
|
| 281 |
+
# FFmpeg command
|
| 282 |
+
# -y: overwrite output
|
| 283 |
+
# -c:v libx264: use H.264 video codec
|
| 284 |
+
# -preset fast: encode speed
|
| 285 |
+
# -profile:v high: high profile for better compatibility
|
| 286 |
+
# -level 4.0: compatibility level
|
| 287 |
+
# -pix_fmt yuv420p: ensure wide player compatibility (essential for QuickTime/Safari)
|
| 288 |
+
# -c:a aac: use AAC audio codec
|
| 289 |
+
# -movflags +faststart: move metadata to front for streaming
|
| 290 |
+
cmd = [
|
| 291 |
+
'ffmpeg', '-y',
|
| 292 |
+
'-i', input_path,
|
| 293 |
+
'-c:v', 'libx264',
|
| 294 |
+
'-preset', 'fast',
|
| 295 |
+
'-pix_fmt', 'yuv420p',
|
| 296 |
+
'-c:a', 'aac',
|
| 297 |
+
'-movflags', '+faststart',
|
| 298 |
+
output_path
|
| 299 |
+
]
|
| 300 |
+
|
| 301 |
+
# Run ffmpeg
|
| 302 |
+
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 303 |
+
|
| 304 |
+
if result.returncode != 0:
|
| 305 |
+
print(f"❌ FFmpeg re-encoding failed: {result.stderr.decode()}")
|
| 306 |
+
return input_path # Fallback to original
|
| 307 |
+
|
| 308 |
+
print(f"✅ Video re-encoded successfully!")
|
| 309 |
+
|
| 310 |
+
# Replace original
|
| 311 |
+
os.remove(input_path)
|
| 312 |
+
os.rename(output_path, input_path)
|
| 313 |
+
return input_path
|
| 314 |
+
|
| 315 |
+
except Exception as e:
|
| 316 |
+
print(f"❌ Error during re-encoding: {e}")
|
| 317 |
+
return input_path
|
| 318 |
+
|
| 319 |
+
@app.route('/api/health', methods=['GET'])
|
| 320 |
+
def health_check():
|
| 321 |
+
"""Health check endpoint with detailed model status"""
|
| 322 |
+
model_status = "ready" if model is not None else "initializing"
|
| 323 |
+
|
| 324 |
+
return jsonify({
|
| 325 |
+
'status': 'healthy',
|
| 326 |
+
'model_status': model_status,
|
| 327 |
+
'model_loaded': model is not None,
|
| 328 |
+
'device': str(device)
|
| 329 |
+
})
|
| 330 |
+
|
| 331 |
+
@app.route('/api/predict', methods=['POST'])
|
| 332 |
+
def predict():
|
| 333 |
+
"""Handle image upload and prediction"""
|
| 334 |
+
try:
|
| 335 |
+
# Check if file is present
|
| 336 |
+
if 'file' not in request.files:
|
| 337 |
+
return jsonify({'error': 'No file provided'}), 400
|
| 338 |
+
|
| 339 |
+
file = request.files['file']
|
| 340 |
+
|
| 341 |
+
if file.filename == '':
|
| 342 |
+
return jsonify({'error': 'No file selected'}), 400
|
| 343 |
+
|
| 344 |
+
if not allowed_file(file.filename):
|
| 345 |
+
return jsonify({'error': 'Invalid file type. Allowed types: png, jpg, jpeg, webp'}), 400
|
| 346 |
+
|
| 347 |
+
# Save file
|
| 348 |
+
filename = secure_filename(file.filename)
|
| 349 |
+
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
| 350 |
+
file.save(filepath)
|
| 351 |
+
|
| 352 |
+
# Make prediction
|
| 353 |
+
result, error = predict_image(filepath)
|
| 354 |
+
|
| 355 |
+
if error:
|
| 356 |
+
return jsonify({'error': error}), 500
|
| 357 |
+
|
| 358 |
+
# Save to History
|
| 359 |
+
import shutil
|
| 360 |
+
history_filename = f"scan_{int(datetime.datetime.now().timestamp())}_{filename}"
|
| 361 |
+
history_path = os.path.join(HISTORY_FOLDER, history_filename)
|
| 362 |
+
|
| 363 |
+
# Copy original file to history folder
|
| 364 |
+
# We need to read the file again or just copy if we haven't deleted it?
|
| 365 |
+
# We read via cv2, the file is still at filepath.
|
| 366 |
+
shutil.copy(filepath, history_path)
|
| 367 |
+
|
| 368 |
+
# Relative path for frontend
|
| 369 |
+
relative_path = f"history_uploads/{history_filename}"
|
| 370 |
+
|
| 371 |
+
scan_id = database.add_scan(
|
| 372 |
+
filename=filename,
|
| 373 |
+
prediction=result['prediction'],
|
| 374 |
+
confidence=result['confidence'],
|
| 375 |
+
fake_prob=result['fake_probability'],
|
| 376 |
+
real_prob=result['real_probability'],
|
| 377 |
+
image_path=relative_path,
|
| 378 |
+
session_id=request.headers.get('X-Session-ID')
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
# Clean up uploaded file
|
| 382 |
+
try:
|
| 383 |
+
os.remove(filepath)
|
| 384 |
+
except:
|
| 385 |
+
pass
|
| 386 |
+
|
| 387 |
+
# Add scan_id to result for frontend tracking
|
| 388 |
+
result['scan_id'] = scan_id
|
| 389 |
+
|
| 390 |
+
return jsonify(result)
|
| 391 |
+
|
| 392 |
+
except Exception as e:
|
| 393 |
+
return jsonify({'error': str(e)}), 500
|
| 394 |
+
|
| 395 |
+
@app.route('/api/predict_video', methods=['POST'])
|
| 396 |
+
def predict_video():
|
| 397 |
+
"""Handle video upload and prediction"""
|
| 398 |
+
try:
|
| 399 |
+
if 'file' not in request.files:
|
| 400 |
+
return jsonify({'error': 'No file provided'}), 400
|
| 401 |
+
|
| 402 |
+
file = request.files['file']
|
| 403 |
+
|
| 404 |
+
if file.filename == '':
|
| 405 |
+
return jsonify({'error': 'No file selected'}), 400
|
| 406 |
+
|
| 407 |
+
if not allowed_file(file.filename):
|
| 408 |
+
return jsonify({'error': 'Invalid file type'}), 400
|
| 409 |
+
|
| 410 |
+
# Save file
|
| 411 |
+
filename = secure_filename(file.filename)
|
| 412 |
+
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
| 413 |
+
file.save(filepath)
|
| 414 |
+
|
| 415 |
+
# Re-encode video for proper web playback
|
| 416 |
+
filepath = reencode_video(filepath)
|
| 417 |
+
|
| 418 |
+
# Process Video
|
| 419 |
+
# Prioritize Optimized ONNX Model
|
| 420 |
+
active_model = video_model_onnx if video_model_onnx is not None else model
|
| 421 |
+
|
| 422 |
+
if active_model is None:
|
| 423 |
+
return jsonify({'error': 'Model not loaded'}), 500
|
| 424 |
+
|
| 425 |
+
result = video_inference.process_video(filepath, active_model, transform, device, frames_per_second=10)
|
| 426 |
+
|
| 427 |
+
if "error" in result:
|
| 428 |
+
return jsonify(result), 500
|
| 429 |
+
|
| 430 |
+
# Save to History (Using the first frame or a placeholder icon for now?)
|
| 431 |
+
# For video, we might want to save the video file itself to history_uploads
|
| 432 |
+
# or just a thumbnail. Let's save the video for now.
|
| 433 |
+
import shutil
|
| 434 |
+
history_filename = f"scan_{int(datetime.datetime.now().timestamp())}_{filename}"
|
| 435 |
+
history_path = os.path.join(HISTORY_FOLDER, history_filename)
|
| 436 |
+
shutil.copy(filepath, history_path)
|
| 437 |
+
|
| 438 |
+
relative_path = f"history_uploads/{history_filename}"
|
| 439 |
+
|
| 440 |
+
# Add to database
|
| 441 |
+
# Note: The database 'add_scan' might expect image-specific fields.
|
| 442 |
+
# We'll re-use 'fake_prob' as 'avg_fake_prob'
|
| 443 |
+
scan_id = database.add_scan(
|
| 444 |
+
filename=filename,
|
| 445 |
+
prediction=result['prediction'],
|
| 446 |
+
confidence=result['confidence'],
|
| 447 |
+
fake_prob=result['avg_fake_prob'],
|
| 448 |
+
real_prob=1 - result['avg_fake_prob'],
|
| 449 |
+
image_path=relative_path,
|
| 450 |
+
session_id=request.headers.get('X-Session-ID')
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
# Clean up
|
| 454 |
+
try:
|
| 455 |
+
os.remove(filepath)
|
| 456 |
+
except:
|
| 457 |
+
pass
|
| 458 |
+
|
| 459 |
+
# Add video URL for frontend playback
|
| 460 |
+
result['video_url'] = relative_path
|
| 461 |
+
result['scan_id'] = scan_id
|
| 462 |
+
|
| 463 |
+
return jsonify(result)
|
| 464 |
+
|
| 465 |
+
except Exception as e:
|
| 466 |
+
print(f"Video Error: {e}")
|
| 467 |
+
return jsonify({'error': str(e)}), 500
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
@app.route('/api/history', methods=['GET'])
|
| 471 |
+
def get_history():
|
| 472 |
+
"""Get all past scans"""
|
| 473 |
+
session_id = request.headers.get('X-Session-ID')
|
| 474 |
+
history = database.get_history(session_id)
|
| 475 |
+
return jsonify(history)
|
| 476 |
+
|
| 477 |
+
@app.route('/api/history/<int:scan_id>', methods=['PATCH'])
|
| 478 |
+
def update_history_item(scan_id):
|
| 479 |
+
"""Update a specific scan's metadata (notes, tags)"""
|
| 480 |
+
data = request.json
|
| 481 |
+
if not data:
|
| 482 |
+
return jsonify({'error': 'No data provided'}), 400
|
| 483 |
+
|
| 484 |
+
if database.update_scan(scan_id, data):
|
| 485 |
+
return jsonify({'message': 'Scan updated successfully'})
|
| 486 |
+
return jsonify({'error': 'Failed to update scan'}), 500
|
| 487 |
+
|
| 488 |
+
@app.route('/api/history/<int:scan_id>', methods=['DELETE'])
|
| 489 |
+
def delete_scan(scan_id):
|
| 490 |
+
"""Delete a specific scan"""
|
| 491 |
+
session_id = request.headers.get('X-Session-ID')
|
| 492 |
+
if database.delete_scan(scan_id, session_id):
|
| 493 |
+
return jsonify({'message': 'Scan deleted'})
|
| 494 |
+
return jsonify({'error': 'Failed to delete scan'}), 500
|
| 495 |
+
|
| 496 |
+
@app.route('/api/history', methods=['DELETE'])
|
| 497 |
+
def clear_history():
|
| 498 |
+
"""Clear all history"""
|
| 499 |
+
session_id = request.headers.get('X-Session-ID')
|
| 500 |
+
if database.clear_history(session_id):
|
| 501 |
+
return jsonify({'message': 'History cleared'})
|
| 502 |
+
return jsonify({'error': 'Failed to clear history'}), 500
|
| 503 |
+
|
| 504 |
+
@app.route('/api/feedback', methods=['POST'])
|
| 505 |
+
def submit_feedback():
|
| 506 |
+
"""Submit user feedback on a prediction"""
|
| 507 |
+
try:
|
| 508 |
+
data = request.json
|
| 509 |
+
if not data:
|
| 510 |
+
return jsonify({'error': 'No data provided'}), 400
|
| 511 |
+
|
| 512 |
+
scan_id = data.get('scan_id')
|
| 513 |
+
is_correct = data.get('is_correct')
|
| 514 |
+
predicted_label = data.get('predicted_label')
|
| 515 |
+
|
| 516 |
+
if scan_id is None or is_correct is None or not predicted_label:
|
| 517 |
+
return jsonify({'error': 'Missing required fields'}), 400
|
| 518 |
+
|
| 519 |
+
# Get scan details from history
|
| 520 |
+
history = database.get_history()
|
| 521 |
+
scan = next((s for s in history if s['id'] == scan_id), None)
|
| 522 |
+
|
| 523 |
+
if not scan:
|
| 524 |
+
return jsonify({'error': 'Scan not found'}), 404
|
| 525 |
+
|
| 526 |
+
actual_label = None
|
| 527 |
+
feedback_image_path = None
|
| 528 |
+
|
| 529 |
+
# If prediction is incorrect, determine actual label and copy image
|
| 530 |
+
if not is_correct:
|
| 531 |
+
# Actual label is opposite of prediction
|
| 532 |
+
actual_label = 'REAL' if predicted_label == 'FAKE' else 'FAKE'
|
| 533 |
+
|
| 534 |
+
# Copy image to feedback folder for retraining
|
| 535 |
+
if scan.get('image_path'):
|
| 536 |
+
try:
|
| 537 |
+
import shutil
|
| 538 |
+
source_path = os.path.join(os.path.dirname(__file__), '..', 'frontend', scan['image_path'])
|
| 539 |
+
feedback_filename = f"feedback_{scan_id}_{scan['filename']}"
|
| 540 |
+
feedback_dest = os.path.join(FEEDBACK_FOLDER, feedback_filename)
|
| 541 |
+
|
| 542 |
+
if os.path.exists(source_path):
|
| 543 |
+
shutil.copy(source_path, feedback_dest)
|
| 544 |
+
feedback_image_path = feedback_filename
|
| 545 |
+
print(f"✅ Copied feedback image to: {feedback_dest}")
|
| 546 |
+
else:
|
| 547 |
+
print(f"⚠️ Source image not found: {source_path}")
|
| 548 |
+
except Exception as e:
|
| 549 |
+
print(f"❌ Error copying feedback image: {e}")
|
| 550 |
+
|
| 551 |
+
# Record feedback in database
|
| 552 |
+
success = database.add_feedback(
|
| 553 |
+
scan_id=scan_id,
|
| 554 |
+
is_correct=is_correct,
|
| 555 |
+
predicted_label=predicted_label,
|
| 556 |
+
actual_label=actual_label,
|
| 557 |
+
image_path=feedback_image_path,
|
| 558 |
+
confidence=scan.get('confidence')
|
| 559 |
+
)
|
| 560 |
+
|
| 561 |
+
if success:
|
| 562 |
+
feedback_type = 'correct' if is_correct else 'incorrect'
|
| 563 |
+
return jsonify({
|
| 564 |
+
'message': f'Feedback recorded successfully',
|
| 565 |
+
'feedback': feedback_type,
|
| 566 |
+
'actual_label': actual_label
|
| 567 |
+
})
|
| 568 |
+
else:
|
| 569 |
+
return jsonify({'error': 'Failed to record feedback'}), 500
|
| 570 |
+
|
| 571 |
+
except Exception as e:
|
| 572 |
+
print(f"Feedback error: {e}")
|
| 573 |
+
return jsonify({'error': str(e)}), 500
|
| 574 |
+
|
| 575 |
+
@app.route('/api/feedback/stats', methods=['GET'])
|
| 576 |
+
def get_feedback_stats():
|
| 577 |
+
"""Get feedback statistics"""
|
| 578 |
+
stats = database.get_feedback_stats()
|
| 579 |
+
return jsonify(stats)
|
| 580 |
+
|
| 581 |
+
@app.route('/api/model-info', methods=['GET'])
|
| 582 |
+
def model_info():
|
| 583 |
+
"""Return model information"""
|
| 584 |
+
return jsonify({
|
| 585 |
+
'model_name': 'DeepGuard: Advanced Deepfake Detector',
|
| 586 |
+
'architecture': 'Hybrid CNN-ViT',
|
| 587 |
+
'components': {
|
| 588 |
+
'RGB Analysis': Config.USE_RGB,
|
| 589 |
+
'Frequency Domain': Config.USE_FREQ,
|
| 590 |
+
'Patch-based Detection': Config.USE_PATCH,
|
| 591 |
+
'Vision Transformer': Config.USE_VIT
|
| 592 |
+
},
|
| 593 |
+
'image_size': Config.IMAGE_SIZE,
|
| 594 |
+
'device': str(device),
|
| 595 |
+
'threshold': 0.5
|
| 596 |
+
})
|
| 597 |
+
|
| 598 |
+
if __name__ == '__main__':
|
| 599 |
+
print("=" * 60)
|
| 600 |
+
print("🚀 DeepGuard - Deepfake Detection System")
|
| 601 |
+
print("=" * 60)
|
| 602 |
+
|
| 603 |
+
# Load model
|
| 604 |
+
load_model()
|
| 605 |
+
|
| 606 |
+
print("=" * 60)
|
| 607 |
+
print("=" * 60)
|
| 608 |
+
# Check if running on Hugging Face Spaces
|
| 609 |
+
if os.environ.get("SPACE_ID"):
|
| 610 |
+
port = 7860
|
| 611 |
+
print(f"🪐 Detected Hugging Face Space. Forcing port {port}")
|
| 612 |
+
else:
|
| 613 |
+
port = int(os.environ.get("PORT", 7860))
|
| 614 |
+
|
| 615 |
+
print(f"🌐 Starting server on http://0.0.0.0:{port}")
|
| 616 |
+
print("=" * 60)
|
| 617 |
+
|
| 618 |
+
app.run(debug=False, host='0.0.0.0', port=port)
|
backend/checkers/__init__.py
ADDED
|
File without changes
|
backend/checkers/metadata_checker.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import exifread
|
| 4 |
+
try:
|
| 5 |
+
import c2pa
|
| 6 |
+
except ImportError:
|
| 7 |
+
c2pa = None
|
| 8 |
+
|
| 9 |
+
def check_metadata(filepath):
|
| 10 |
+
"""
|
| 11 |
+
Checks for Content Credentials (C2PA) and specific AI-generation metadata in Exif/XMP.
|
| 12 |
+
Returns a dictionary with detection status and details.
|
| 13 |
+
"""
|
| 14 |
+
result = {
|
| 15 |
+
"detected": False,
|
| 16 |
+
"method": None,
|
| 17 |
+
"source": None,
|
| 18 |
+
"details": {}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
# 1. Check C2PA / Content Credentials
|
| 22 |
+
if c2pa:
|
| 23 |
+
try:
|
| 24 |
+
# Correct API usage for c2pa-python
|
| 25 |
+
reader = c2pa.Reader(filepath)
|
| 26 |
+
manifest_json = reader.json()
|
| 27 |
+
|
| 28 |
+
# Robust check: Convert to string and search for keywords
|
| 29 |
+
# This avoids dependency on exact JSON structure which might vary
|
| 30 |
+
json_str = manifest_json.lower()
|
| 31 |
+
|
| 32 |
+
if "dall-e" in json_str:
|
| 33 |
+
result["detected"] = True
|
| 34 |
+
result["method"] = "C2PA"
|
| 35 |
+
result["source"] = "DALL-E"
|
| 36 |
+
return result
|
| 37 |
+
if "adobe firefly" in json_str:
|
| 38 |
+
result["detected"] = True
|
| 39 |
+
result["method"] = "C2PA"
|
| 40 |
+
result["source"] = "Adobe Firefly"
|
| 41 |
+
return result
|
| 42 |
+
if "bing image creator" in json_str:
|
| 43 |
+
result["detected"] = True
|
| 44 |
+
result["method"] = "C2PA"
|
| 45 |
+
result["source"] = "Bing Image Creator"
|
| 46 |
+
return result
|
| 47 |
+
if "my-tool" in json_str: # Example placeholder
|
| 48 |
+
pass
|
| 49 |
+
|
| 50 |
+
# General check for "artificial" or "created" actions if no specific tool found
|
| 51 |
+
if 'c2pa.actions' in json_str and 'artificial' in json_str:
|
| 52 |
+
result["detected"] = True
|
| 53 |
+
result["method"] = "C2PA"
|
| 54 |
+
result["source"] = "AI Generated (C2PA)"
|
| 55 |
+
return result
|
| 56 |
+
|
| 57 |
+
except Exception as e:
|
| 58 |
+
# Expected if no C2PA manifest exists
|
| 59 |
+
# print(f"C2PA Check Info: {e}")
|
| 60 |
+
pass
|
| 61 |
+
|
| 62 |
+
# 2. Check Exif/XMP via ExifRead
|
| 63 |
+
try:
|
| 64 |
+
with open(filepath, 'rb') as f:
|
| 65 |
+
tags = exifread.process_file(f)
|
| 66 |
+
|
| 67 |
+
# Common AI signatures in Exif/XMP/IPTC
|
| 68 |
+
software_tags = [str(tags.get('Image Software', '')), str(tags.get('0th Software', ''))]
|
| 69 |
+
description_tags = [str(tags.get('Image ImageDescription', '')), str(tags.get('EXIF UserComment', ''))]
|
| 70 |
+
|
| 71 |
+
# DALL-E 3 often leaves signature in ImageDescription or Software
|
| 72 |
+
for tag in software_tags + description_tags:
|
| 73 |
+
tag_lower = tag.lower()
|
| 74 |
+
if "dall-e" in tag_lower:
|
| 75 |
+
result["detected"] = True
|
| 76 |
+
result["method"] = "EXIF"
|
| 77 |
+
result["source"] = "DALL-E"
|
| 78 |
+
return result
|
| 79 |
+
if "adobe firefly" in tag_lower:
|
| 80 |
+
result["detected"] = True
|
| 81 |
+
result["method"] = "EXIF"
|
| 82 |
+
result["source"] = "Adobe Firefly"
|
| 83 |
+
return result
|
| 84 |
+
if "bing image creator" in tag_lower:
|
| 85 |
+
result["detected"] = True
|
| 86 |
+
result["method"] = "EXIF"
|
| 87 |
+
result["source"] = "Bing Image Creator"
|
| 88 |
+
return result
|
| 89 |
+
if "stable diffusion" in tag_lower:
|
| 90 |
+
result["detected"] = True
|
| 91 |
+
result["method"] = "EXIF"
|
| 92 |
+
result["source"] = "Stable Diffusion"
|
| 93 |
+
return result
|
| 94 |
+
|
| 95 |
+
# Generic check for other known AI tools based on common signatures
|
| 96 |
+
for tool in ["midjourney", "runway", "leonardo", "nightcafe", "canva"]:
|
| 97 |
+
if tool in tag_lower:
|
| 98 |
+
result["detected"] = True
|
| 99 |
+
result["method"] = "EXIF"
|
| 100 |
+
result["source"] = tool.title() # Capitalize first letter
|
| 101 |
+
return result
|
| 102 |
+
|
| 103 |
+
except Exception as e:
|
| 104 |
+
print(f"Exif Check Error: {e}")
|
| 105 |
+
|
| 106 |
+
# 3. Check PNG Text Chunks (often used by Leonardo, NightCafe, Stable Diffusion)
|
| 107 |
+
# ExifRead doesn't always catch purely textual PNG chunks "parameters" or "Software"
|
| 108 |
+
try:
|
| 109 |
+
from PIL import Image
|
| 110 |
+
img = Image.open(filepath)
|
| 111 |
+
img.load() # Load to access info
|
| 112 |
+
|
| 113 |
+
info = img.info or {}
|
| 114 |
+
|
| 115 |
+
# Combine all string values for search
|
| 116 |
+
search_space = " ".join([str(v).lower() for k, v in info.items()])
|
| 117 |
+
|
| 118 |
+
if "stable diffusion" in search_space:
|
| 119 |
+
result["detected"] = True
|
| 120 |
+
result["method"] = "PNG Metadata"
|
| 121 |
+
result["source"] = "Stable Diffusion"
|
| 122 |
+
return result
|
| 123 |
+
|
| 124 |
+
if "midjourney" in search_space:
|
| 125 |
+
result["detected"] = True
|
| 126 |
+
result["method"] = "PNG Metadata"
|
| 127 |
+
result["source"] = "Midjourney"
|
| 128 |
+
return result
|
| 129 |
+
|
| 130 |
+
if "leonardo" in search_space:
|
| 131 |
+
result["detected"] = True
|
| 132 |
+
result["method"] = "PNG Metadata"
|
| 133 |
+
result["source"] = "Leonardo AI"
|
| 134 |
+
return result
|
| 135 |
+
|
| 136 |
+
if "nightcafe" in search_space:
|
| 137 |
+
result["detected"] = True
|
| 138 |
+
result["method"] = "PNG Metadata"
|
| 139 |
+
result["source"] = "NightCafe"
|
| 140 |
+
return result
|
| 141 |
+
|
| 142 |
+
if "runway" in search_space:
|
| 143 |
+
result["detected"] = True
|
| 144 |
+
result["method"] = "PNG Metadata"
|
| 145 |
+
result["source"] = "Runway Gen-2"
|
| 146 |
+
return result
|
| 147 |
+
|
| 148 |
+
except Exception as e:
|
| 149 |
+
# print(f"PNG Check Error: {e}")
|
| 150 |
+
pass
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
return result
|
backend/checkers/watermark_checker.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
try:
|
| 3 |
+
from imwatermark import WatermarkDecoder
|
| 4 |
+
except ImportError:
|
| 5 |
+
WatermarkDecoder = None
|
| 6 |
+
|
| 7 |
+
def check_watermarks(filepath):
|
| 8 |
+
"""
|
| 9 |
+
Checks for invisible watermarks (specifically Stable Diffusion's 'sd_private').
|
| 10 |
+
Returns a dictionary with detection status.
|
| 11 |
+
"""
|
| 12 |
+
result = {
|
| 13 |
+
"detected": False,
|
| 14 |
+
"method": None,
|
| 15 |
+
"source": None
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
if not WatermarkDecoder:
|
| 19 |
+
return result
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
# Standard Stable Diffusion watermark is 48 bits, detecting 'bytes'
|
| 23 |
+
decoder = WatermarkDecoder('bytes', 32) # Standard length for some, but SD often uses 48 bits?
|
| 24 |
+
# Actually, the 'invisible-watermark' library default for SD
|
| 25 |
+
# typically uses method='dwtDct' combined with a specific decoder.
|
| 26 |
+
|
| 27 |
+
# Let's try the standard approach for Stable Diffusion detection
|
| 28 |
+
# The library usually has a specific 'bytes' decoder for it.
|
| 29 |
+
|
| 30 |
+
bgr_image = None
|
| 31 |
+
import cv2
|
| 32 |
+
bgr_image = cv2.imread(filepath)
|
| 33 |
+
if bgr_image is None:
|
| 34 |
+
return result
|
| 35 |
+
|
| 36 |
+
decoder = WatermarkDecoder('bytes', 136) # Try generic length or specific
|
| 37 |
+
watermark = decoder.decode(bgr_image, 'dwtDct')
|
| 38 |
+
|
| 39 |
+
# Stable Diffusion's watermark often decodes to explicit bytes.
|
| 40 |
+
# However, a more robust way often used is checking for the specific signature
|
| 41 |
+
# that the library 'invisible-watermark' looks for.
|
| 42 |
+
|
| 43 |
+
# Simplifying: If we decode *something* valid/structured, it might be watermarked.
|
| 44 |
+
# But for 'sd_private', we verify specifically.
|
| 45 |
+
|
| 46 |
+
# Note: A simpler check using the library's built-in script logic:
|
| 47 |
+
# It usually converts "Stability AI" string to bits?
|
| 48 |
+
|
| 49 |
+
# If we successfully decode the known string "Stability" or derivatives.
|
| 50 |
+
decoded_text = watermark.decode('utf-8', errors='ignore')
|
| 51 |
+
|
| 52 |
+
if "Stability" in decoded_text or "sd_private" in decoded_text :
|
| 53 |
+
result["detected"] = True
|
| 54 |
+
result["method"] = "Invisible Watermark"
|
| 55 |
+
result["source"] = "Stable Diffusion"
|
| 56 |
+
|
| 57 |
+
except Exception as e:
|
| 58 |
+
# print(f"Watermark Check Error: {e}")
|
| 59 |
+
pass
|
| 60 |
+
|
| 61 |
+
return result
|
backend/database.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import datetime
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
DB_NAME = os.path.join(os.path.dirname(__file__), 'database.db')
|
| 6 |
+
|
| 7 |
+
def get_db_connection():
|
| 8 |
+
try:
|
| 9 |
+
conn = sqlite3.connect(DB_NAME)
|
| 10 |
+
conn.row_factory = sqlite3.Row
|
| 11 |
+
return conn
|
| 12 |
+
except sqlite3.Error as e:
|
| 13 |
+
print(f"Database error: {e}")
|
| 14 |
+
return None
|
| 15 |
+
|
| 16 |
+
def init_db():
|
| 17 |
+
conn = get_db_connection()
|
| 18 |
+
if conn:
|
| 19 |
+
try:
|
| 20 |
+
conn.execute('''
|
| 21 |
+
CREATE TABLE IF NOT EXISTS history (
|
| 22 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 23 |
+
filename TEXT NOT NULL,
|
| 24 |
+
prediction TEXT NOT NULL,
|
| 25 |
+
confidence REAL NOT NULL,
|
| 26 |
+
fake_probability REAL NOT NULL,
|
| 27 |
+
real_probability REAL NOT NULL,
|
| 28 |
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 29 |
+
)
|
| 30 |
+
''')
|
| 31 |
+
conn.commit()
|
| 32 |
+
print("✅ Database initialized successfully.")
|
| 33 |
+
except sqlite3.Error as e:
|
| 34 |
+
print(f"Error initializing database: {e}")
|
| 35 |
+
|
| 36 |
+
# Migration: Add image_path, notes, tags if not exists
|
| 37 |
+
try:
|
| 38 |
+
conn.execute('ALTER TABLE history ADD COLUMN image_path TEXT')
|
| 39 |
+
print("✅ Added image_path column.")
|
| 40 |
+
except sqlite3.Error:
|
| 41 |
+
pass # Column likely exists
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
conn.execute('ALTER TABLE history ADD COLUMN notes TEXT')
|
| 45 |
+
print("✅ Added notes column.")
|
| 46 |
+
except sqlite3.Error:
|
| 47 |
+
pass
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
conn.execute('ALTER TABLE history ADD COLUMN tags TEXT')
|
| 51 |
+
print("✅ Added tags column.")
|
| 52 |
+
except sqlite3.Error:
|
| 53 |
+
pass
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
conn.execute('ALTER TABLE history ADD COLUMN session_id TEXT')
|
| 57 |
+
print("✅ Added session_id column.")
|
| 58 |
+
except sqlite3.Error:
|
| 59 |
+
pass
|
| 60 |
+
|
| 61 |
+
# Create feedback table for user feedback on predictions
|
| 62 |
+
try:
|
| 63 |
+
conn.execute('''
|
| 64 |
+
CREATE TABLE IF NOT EXISTS feedback (
|
| 65 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 66 |
+
scan_id INTEGER NOT NULL,
|
| 67 |
+
user_feedback TEXT NOT NULL,
|
| 68 |
+
predicted_label TEXT NOT NULL,
|
| 69 |
+
actual_label TEXT,
|
| 70 |
+
image_path TEXT,
|
| 71 |
+
confidence REAL,
|
| 72 |
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 73 |
+
FOREIGN KEY (scan_id) REFERENCES history(id)
|
| 74 |
+
)
|
| 75 |
+
''')
|
| 76 |
+
conn.commit()
|
| 77 |
+
print("✅ Feedback table initialized successfully.")
|
| 78 |
+
except sqlite3.Error as e:
|
| 79 |
+
print(f"Error initializing feedback table: {e}")
|
| 80 |
+
|
| 81 |
+
finally:
|
| 82 |
+
conn.close()
|
| 83 |
+
|
| 84 |
+
def add_scan(filename, prediction, confidence, fake_prob, real_prob, image_path="", session_id=None):
|
| 85 |
+
conn = get_db_connection()
|
| 86 |
+
if conn:
|
| 87 |
+
try:
|
| 88 |
+
cursor = conn.execute('''
|
| 89 |
+
INSERT INTO history (filename, prediction, confidence, fake_probability, real_probability, image_path, session_id)
|
| 90 |
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 91 |
+
''', (filename, prediction, confidence, fake_prob, real_prob, image_path, session_id))
|
| 92 |
+
conn.commit()
|
| 93 |
+
scan_id = cursor.lastrowid
|
| 94 |
+
return scan_id
|
| 95 |
+
except sqlite3.Error as e:
|
| 96 |
+
print(f"Error adding scan: {e}")
|
| 97 |
+
return None
|
| 98 |
+
finally:
|
| 99 |
+
conn.close()
|
| 100 |
+
return None
|
| 101 |
+
|
| 102 |
+
def get_history(session_id=None):
|
| 103 |
+
conn = get_db_connection()
|
| 104 |
+
if conn:
|
| 105 |
+
try:
|
| 106 |
+
query = 'SELECT * FROM history'
|
| 107 |
+
params = []
|
| 108 |
+
if session_id:
|
| 109 |
+
query += ' WHERE session_id = ? OR session_id IS NULL' # Allow seeing public/legacy items if desired, or strictly session specific
|
| 110 |
+
# Strict session isolation:
|
| 111 |
+
query = 'SELECT * FROM history WHERE session_id = ?'
|
| 112 |
+
params = [session_id]
|
| 113 |
+
else:
|
| 114 |
+
# If no session_id provided (legacy behavior), maybe show all or none?
|
| 115 |
+
# Let's show only items with NULL session_id to avoid leaking user data
|
| 116 |
+
query = 'SELECT * FROM history WHERE session_id IS NULL'
|
| 117 |
+
|
| 118 |
+
query += ' ORDER BY timestamp DESC'
|
| 119 |
+
cursor = conn.execute(query, params)
|
| 120 |
+
history = [dict(row) for row in cursor.fetchall()]
|
| 121 |
+
return history
|
| 122 |
+
except sqlite3.Error as e:
|
| 123 |
+
print(f"Error retrieving history: {e}")
|
| 124 |
+
return []
|
| 125 |
+
finally:
|
| 126 |
+
conn.close()
|
| 127 |
+
return []
|
| 128 |
+
|
| 129 |
+
def clear_history(session_id=None):
|
| 130 |
+
conn = get_db_connection()
|
| 131 |
+
if conn:
|
| 132 |
+
try:
|
| 133 |
+
if session_id:
|
| 134 |
+
conn.execute('DELETE FROM history WHERE session_id = ?', (session_id,))
|
| 135 |
+
else:
|
| 136 |
+
conn.execute('DELETE FROM history WHERE session_id IS NULL')
|
| 137 |
+
conn.commit()
|
| 138 |
+
return True
|
| 139 |
+
except sqlite3.Error as e:
|
| 140 |
+
print(f"Error clearing history: {e}")
|
| 141 |
+
return False
|
| 142 |
+
finally:
|
| 143 |
+
conn.close()
|
| 144 |
+
return False
|
| 145 |
+
|
| 146 |
+
def delete_scan(scan_id, session_id=None):
|
| 147 |
+
conn = get_db_connection()
|
| 148 |
+
if conn:
|
| 149 |
+
try:
|
| 150 |
+
if session_id:
|
| 151 |
+
conn.execute('DELETE FROM history WHERE id = ? AND session_id = ?', (scan_id, session_id))
|
| 152 |
+
else:
|
| 153 |
+
conn.execute('DELETE FROM history WHERE id = ? AND session_id IS NULL', (scan_id,))
|
| 154 |
+
conn.commit()
|
| 155 |
+
return True
|
| 156 |
+
except sqlite3.Error as e:
|
| 157 |
+
print(f"Error deleting scan: {e}")
|
| 158 |
+
return False
|
| 159 |
+
finally:
|
| 160 |
+
conn.close()
|
| 161 |
+
return False
|
| 162 |
+
|
| 163 |
+
def update_scan(scan_id, data):
|
| 164 |
+
conn = get_db_connection()
|
| 165 |
+
if conn:
|
| 166 |
+
try:
|
| 167 |
+
fields = []
|
| 168 |
+
values = []
|
| 169 |
+
if 'notes' in data:
|
| 170 |
+
fields.append("notes = ?")
|
| 171 |
+
values.append(data['notes'])
|
| 172 |
+
if 'tags' in data:
|
| 173 |
+
fields.append("tags = ?")
|
| 174 |
+
values.append(data['tags'])
|
| 175 |
+
|
| 176 |
+
if not fields:
|
| 177 |
+
return True
|
| 178 |
+
|
| 179 |
+
values.append(scan_id)
|
| 180 |
+
query = f"UPDATE history SET {', '.join(fields)} WHERE id = ?"
|
| 181 |
+
conn.execute(query, tuple(values))
|
| 182 |
+
conn.commit()
|
| 183 |
+
return True
|
| 184 |
+
except sqlite3.Error as e:
|
| 185 |
+
print(f"Error updating scan: {e}")
|
| 186 |
+
return False
|
| 187 |
+
finally:
|
| 188 |
+
conn.close()
|
| 189 |
+
return False
|
| 190 |
+
|
| 191 |
+
def add_feedback(scan_id, is_correct, predicted_label, actual_label=None, image_path=None, confidence=None):
|
| 192 |
+
"""Record user feedback on a prediction"""
|
| 193 |
+
conn = get_db_connection()
|
| 194 |
+
if conn:
|
| 195 |
+
try:
|
| 196 |
+
user_feedback = 'correct' if is_correct else 'incorrect'
|
| 197 |
+
conn.execute('''
|
| 198 |
+
INSERT INTO feedback (scan_id, user_feedback, predicted_label, actual_label, image_path, confidence)
|
| 199 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 200 |
+
''', (scan_id, user_feedback, predicted_label, actual_label, image_path, confidence))
|
| 201 |
+
conn.commit()
|
| 202 |
+
return True
|
| 203 |
+
except sqlite3.Error as e:
|
| 204 |
+
print(f"Error adding feedback: {e}")
|
| 205 |
+
return False
|
| 206 |
+
finally:
|
| 207 |
+
conn.close()
|
| 208 |
+
return False
|
| 209 |
+
|
| 210 |
+
def get_incorrect_predictions():
|
| 211 |
+
"""Get all incorrect predictions for model retraining"""
|
| 212 |
+
conn = get_db_connection()
|
| 213 |
+
if conn:
|
| 214 |
+
try:
|
| 215 |
+
cursor = conn.execute('''
|
| 216 |
+
SELECT f.*, h.filename
|
| 217 |
+
FROM feedback f
|
| 218 |
+
LEFT JOIN history h ON f.scan_id = h.id
|
| 219 |
+
WHERE f.user_feedback = 'incorrect'
|
| 220 |
+
ORDER BY f.timestamp DESC
|
| 221 |
+
''')
|
| 222 |
+
incorrect = [dict(row) for row in cursor.fetchall()]
|
| 223 |
+
return incorrect
|
| 224 |
+
except sqlite3.Error as e:
|
| 225 |
+
print(f"Error retrieving incorrect predictions: {e}")
|
| 226 |
+
return []
|
| 227 |
+
finally:
|
| 228 |
+
conn.close()
|
| 229 |
+
return []
|
| 230 |
+
|
| 231 |
+
def get_feedback_stats():
|
| 232 |
+
"""Get statistics on user feedback"""
|
| 233 |
+
conn = get_db_connection()
|
| 234 |
+
if conn:
|
| 235 |
+
try:
|
| 236 |
+
cursor = conn.execute('''
|
| 237 |
+
SELECT
|
| 238 |
+
COUNT(*) as total_feedback,
|
| 239 |
+
SUM(CASE WHEN user_feedback = 'correct' THEN 1 ELSE 0 END) as correct_count,
|
| 240 |
+
SUM(CASE WHEN user_feedback = 'incorrect' THEN 1 ELSE 0 END) as incorrect_count
|
| 241 |
+
FROM feedback
|
| 242 |
+
''')
|
| 243 |
+
stats = dict(cursor.fetchone())
|
| 244 |
+
return stats
|
| 245 |
+
except sqlite3.Error as e:
|
| 246 |
+
print(f"Error retrieving feedback stats: {e}")
|
| 247 |
+
return {'total_feedback': 0, 'correct_count': 0, 'incorrect_count': 0}
|
| 248 |
+
finally:
|
| 249 |
+
conn.close()
|
| 250 |
+
return {'total_feedback': 0, 'correct_count': 0, 'incorrect_count': 0}
|
| 251 |
+
|
| 252 |
+
# Initialize DB on module load
|
| 253 |
+
init_db()
|
backend/requirements_web.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask==3.0.0
|
| 2 |
+
flask-cors==4.0.0
|
| 3 |
+
torch
|
| 4 |
+
torchvision
|
| 5 |
+
opencv-python
|
| 6 |
+
albumentations
|
| 7 |
+
Pillow
|
| 8 |
+
numpy
|
| 9 |
+
safetensors
|
| 10 |
+
c2pa-python
|
| 11 |
+
invisible-watermark==0.2.0
|
| 12 |
+
ExifRead
|
frontend/accuracy_icon.png
ADDED
|
|
Git LFS Details
|
frontend/analysis.html
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
| 7 |
+
<title>Analysis Dashboard - DeepGuard</title>
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link
|
| 11 |
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Grotesk:wght@400;500;600;700&display=swap"
|
| 12 |
+
rel="stylesheet">
|
| 13 |
+
<link rel="stylesheet" href="variables.css">
|
| 14 |
+
<link rel="stylesheet" href="style.css">
|
| 15 |
+
<link rel="stylesheet" href="animations.css">
|
| 16 |
+
<link rel="stylesheet" href="pwa.css">
|
| 17 |
+
<link rel="stylesheet" href="responsive-additions.css">
|
| 18 |
+
<link rel="stylesheet" href="responsive-pages.css">
|
| 19 |
+
|
| 20 |
+
<!-- PWA Manifest -->
|
| 21 |
+
<link rel="manifest" href="manifest.json">
|
| 22 |
+
<meta name="theme-color" content="#E3F514">
|
| 23 |
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
| 24 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
| 25 |
+
<meta name="apple-mobile-web-app-title" content="DeepGuard">
|
| 26 |
+
<link rel="apple-touch-icon" href="logo.ico">
|
| 27 |
+
</head>
|
| 28 |
+
|
| 29 |
+
<body class="analysis-page">
|
| 30 |
+
<div class="mesh-background"></div>
|
| 31 |
+
<div id="particles-js"></div>
|
| 32 |
+
|
| 33 |
+
<!-- Navigation -->
|
| 34 |
+
<nav class="navbar">
|
| 35 |
+
<div class="container">
|
| 36 |
+
<div class="nav-content">
|
| 37 |
+
<a href="index.html" class="logo">
|
| 38 |
+
<img src="logo.svg" alt="DeepGuard Logo" class="logo-img">
|
| 39 |
+
<span class="logo-text">Deep<span class="gradient-text">Guard</span></span>
|
| 40 |
+
</a>
|
| 41 |
+
|
| 42 |
+
<!-- Hamburger Menu Button (Mobile) -->
|
| 43 |
+
<button class="hamburger" id="hamburger" aria-label="Toggle navigation menu">
|
| 44 |
+
<span></span>
|
| 45 |
+
<span></span>
|
| 46 |
+
<span></span>
|
| 47 |
+
</button>
|
| 48 |
+
|
| 49 |
+
<!-- Navigation Menu -->
|
| 50 |
+
<div class="nav-menu-wrapper">
|
| 51 |
+
<ul class="nav-menu">
|
| 52 |
+
<li><a href="index.html">Home</a></li>
|
| 53 |
+
<li><a href="analysis.html" class="active">Analysis</a></li>
|
| 54 |
+
<li><a href="history.html">History</a></li>
|
| 55 |
+
</ul>
|
| 56 |
+
<a href="index.html" class="btn-secondary-nav">← Back to Home</a>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
</nav>
|
| 61 |
+
|
| 62 |
+
<main class="analysis-container">
|
| 63 |
+
<div class="analysis-grid">
|
| 64 |
+
<!-- Left Side: Upload & Preview -->
|
| 65 |
+
<div class="upload-section">
|
| 66 |
+
<div class="section-header-small">
|
| 67 |
+
<h2>Media Input</h2>
|
| 68 |
+
<p>Upload image for AI analysis</p>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<div class="upload-area animate-pulse-glow" id="uploadArea">
|
| 72 |
+
<div class="upload-icon animate-float">
|
| 73 |
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
| 74 |
+
stroke-width="2">
|
| 75 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
| 76 |
+
<polyline points="17 8 12 3 7 8" />
|
| 77 |
+
<line x1="12" y1="3" x2="12" y2="15" />
|
| 78 |
+
</svg>
|
| 79 |
+
</div>
|
| 80 |
+
<h3 class="upload-title">Drop Images Here</h3>
|
| 81 |
+
<p class="upload-description">Supports JPG, PNG, WEBP, MP4, AVI, MOV · Max 100MB</p>
|
| 82 |
+
<p class="upload-description" style="margin-top: 8px; font-size: 13px; opacity: 0.8;">📋 Or press
|
| 83 |
+
Ctrl/Cmd+V to paste from clipboard</p>
|
| 84 |
+
<p class="file-count-badge" id="fileCountBadge" style="display: none;"></p>
|
| 85 |
+
<input type="file" id="fileInput" accept="image/*,video/*" multiple hidden>
|
| 86 |
+
<button class="btn-primary" onclick="document.getElementById('fileInput').click()"
|
| 87 |
+
aria-label="Select files to upload">Select
|
| 88 |
+
Files</button>
|
| 89 |
+
<!-- Scanner Line Effect -->
|
| 90 |
+
<div class="scanner-line"></div>
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
<!-- File Queue Preview -->
|
| 94 |
+
<div class="file-queue-container" id="fileQueueContainer" style="display: none;">
|
| 95 |
+
<div class="queue-header">
|
| 96 |
+
<div class="queue-title-section">
|
| 97 |
+
<h3 class="queue-title">
|
| 98 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
| 99 |
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-inline"
|
| 100 |
+
aria-hidden="true">
|
| 101 |
+
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
|
| 102 |
+
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
|
| 103 |
+
</svg>
|
| 104 |
+
Upload Queue
|
| 105 |
+
</h3>
|
| 106 |
+
<span class="queue-count-badge" id="queueCount"
|
| 107 |
+
aria-label="Number of files in queue">0</span>
|
| 108 |
+
</div>
|
| 109 |
+
<div class="queue-actions">
|
| 110 |
+
<button class="btn-secondary-small" onclick="document.getElementById('fileInput').click()"
|
| 111 |
+
title="Add more files" aria-label="Add more files to queue">
|
| 112 |
+
+ Add More
|
| 113 |
+
</button>
|
| 114 |
+
<button class="btn-secondary-small" onclick="clearQueue()" title="Clear all files"
|
| 115 |
+
aria-label="Clear all files from queue">
|
| 116 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
| 117 |
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-inline"
|
| 118 |
+
aria-hidden="true">
|
| 119 |
+
<polyline points="3 6 5 6 21 6"></polyline>
|
| 120 |
+
<path
|
| 121 |
+
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
|
| 122 |
+
</path>
|
| 123 |
+
<line x1="10" y1="11" x2="10" y2="17"></line>
|
| 124 |
+
<line x1="14" y1="11" x2="14" y2="17"></line>
|
| 125 |
+
</svg>
|
| 126 |
+
Clear
|
| 127 |
+
</button>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
<div class="file-queue" id="fileQueue"></div>
|
| 131 |
+
<div class="queue-footer">
|
| 132 |
+
<button class="btn-secondary-outline" onclick="clearQueue()" style="flex: 1;"
|
| 133 |
+
aria-label="Back to upload area">
|
| 134 |
+
← Back to Upload
|
| 135 |
+
</button>
|
| 136 |
+
<button class="btn-primary" onclick="processUploadQueue()" style="flex: 2;" id="startUploadBtn"
|
| 137 |
+
aria-label="Start analysis processing">
|
| 138 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
| 139 |
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-inline"
|
| 140 |
+
aria-hidden="true">
|
| 141 |
+
<path
|
| 142 |
+
d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z">
|
| 143 |
+
</path>
|
| 144 |
+
<path
|
| 145 |
+
d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z">
|
| 146 |
+
</path>
|
| 147 |
+
<path d="M9 12H4s.55-3.03 2-5c1.62-2.2 5-3 5-3"></path>
|
| 148 |
+
<path d="M12 15v5s3.03-.55 5-2c2.2-1.62 3-5 3-5"></path>
|
| 149 |
+
</svg>
|
| 150 |
+
Start Analysis
|
| 151 |
+
</button>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
|
| 155 |
+
<div class="preview-area" id="previewArea" style="display: none;">
|
| 156 |
+
<img id="previewImage" src="" alt="Preview">
|
| 157 |
+
<img id="heatmapOverlay" src="" alt="Heatmap"
|
| 158 |
+
style="display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; opacity: 0; transition: opacity 0.5s ease;">
|
| 159 |
+
|
| 160 |
+
<div class="heatmap-toggle-container" id="heatmapToggle" style="display: none;">
|
| 161 |
+
<span class="toggle-label">Heatmap</span>
|
| 162 |
+
<label class="switch">
|
| 163 |
+
<input type="checkbox" id="heatmapSwitch">
|
| 164 |
+
<span class="slider round"></span>
|
| 165 |
+
</label>
|
| 166 |
+
</div>
|
| 167 |
+
|
| 168 |
+
<button class="btn-close-preview" onclick="resetAnalysis()" aria-label="Close preview"
|
| 169 |
+
title="Close Preview">×</button>
|
| 170 |
+
</div>
|
| 171 |
+
<!-- Multi-Stage Processing Overlay -->
|
| 172 |
+
<div id="processingOverlay" class="processing-overlay-enhanced" style="display: none;">
|
| 173 |
+
<div class="processing-content-enhanced">
|
| 174 |
+
<!-- Model Status Badge -->
|
| 175 |
+
<div class="model-status-badge" id="modelStatusBadge" style="display: none;">
|
| 176 |
+
<span class="status-dot"></span>
|
| 177 |
+
<span class="status-text">Model Status: <strong
|
| 178 |
+
id="modelStatusText">Checking...</strong></span>
|
| 179 |
+
</div>
|
| 180 |
+
|
| 181 |
+
<!-- Spinner -->
|
| 182 |
+
<div class="neural-spinner">
|
| 183 |
+
<div class="spinner-ring"></div>
|
| 184 |
+
<div class="spinner-ring"></div>
|
| 185 |
+
<div class="spinner-ring"></div>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
<!-- Main Status Title -->
|
| 189 |
+
<h2 class="processing-title-enhanced" id="processingMainTitle">Analyzing Media</h2>
|
| 190 |
+
|
| 191 |
+
<!-- Step Progress Indicators -->
|
| 192 |
+
<div class="progress-steps" id="progressSteps">
|
| 193 |
+
<div class="progress-step" data-step="upload">
|
| 194 |
+
<div class="step-icon">📤</div>
|
| 195 |
+
<div class="step-label">Uploading</div>
|
| 196 |
+
</div>
|
| 197 |
+
<div class="progress-step" data-step="connect">
|
| 198 |
+
<div class="step-icon">🔌</div>
|
| 199 |
+
<div class="step-label">Connecting</div>
|
| 200 |
+
</div>
|
| 201 |
+
<div class="progress-step" data-step="warmup">
|
| 202 |
+
<div class="step-icon">⏳</div>
|
| 203 |
+
<div class="step-label">Warming Up</div>
|
| 204 |
+
</div>
|
| 205 |
+
<div class="progress-step" data-step="analyze">
|
| 206 |
+
<div class="step-icon">🔍</div>
|
| 207 |
+
<div class="step-label">Analyzing</div>
|
| 208 |
+
</div>
|
| 209 |
+
<div class="progress-step" data-step="generate">
|
| 210 |
+
<div class="step-icon">✨</div>
|
| 211 |
+
<div class="step-label">Finalizing</div>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
|
| 215 |
+
<!-- Status Message -->
|
| 216 |
+
<p class="processing-message" id="processingMessage">
|
| 217 |
+
Please don't refresh • Your media is safe and never stored
|
| 218 |
+
</p>
|
| 219 |
+
|
| 220 |
+
<!-- Warm-Up Alert (Hidden by default) -->
|
| 221 |
+
<div class="warmup-alert" id="warmupAlert" style="display: none;">
|
| 222 |
+
<div class="warmup-icon">⏳</div>
|
| 223 |
+
<div class="warmup-content">
|
| 224 |
+
<div class="warmup-progress-wrapper" style="width: 100%;">
|
| 225 |
+
<div
|
| 226 |
+
style="display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 14px; font-weight: 500; color: #fff;">
|
| 227 |
+
<span>Model Waking Up</span>
|
| 228 |
+
<span id="warmupPercent">0%</span>
|
| 229 |
+
</div>
|
| 230 |
+
<div
|
| 231 |
+
style="width: 100%; height: 6px; background: rgba(255,255,255,0.1); border-radius: 10px; overflow: hidden; position: relative;">
|
| 232 |
+
<div id="warmupProgressFill"
|
| 233 |
+
style="width: 0%; height: 100%; background: #E3F514; border-radius: 10px; transition: width 0.3s linear; box-shadow: 0 0 10px rgba(227, 245, 20, 0.5);">
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
<p class="warmup-reassurance"
|
| 237 |
+
style="margin-top: 10px; font-size: 12px; color: rgba(255,255,255,0.6);">✓
|
| 238 |
+
Starting cloud instance (first run only)</p>
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
</div>
|
| 245 |
+
|
| 246 |
+
<!-- Right Side: Results & Metrics -->
|
| 247 |
+
<div class="results-section" id="resultsSection">
|
| 248 |
+
<div class="empty-state">
|
| 249 |
+
<div class="empty-icon animate-float" aria-hidden="true"
|
| 250 |
+
style="opacity: 0.6; color: var(--accent-yellow);">
|
| 251 |
+
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
| 252 |
+
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
| 253 |
+
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
|
| 254 |
+
</svg>
|
| 255 |
+
</div>
|
| 256 |
+
<h3 style="font-family: var(--font-display); font-size: 1.5rem; margin-bottom: 8px;">Ready to
|
| 257 |
+
Analyze</h3>
|
| 258 |
+
<p style="max-width: 280px; margin: 0 auto; line-height: 1.6;">Upload media to start the DeepGuard
|
| 259 |
+
detection pipeline</p>
|
| 260 |
+
</div>
|
| 261 |
+
|
| 262 |
+
<div class="analysis-results" style="display: none;">
|
| 263 |
+
<div class="verdict-card" id="verdictCard">
|
| 264 |
+
<span class="verdict-label">DETECTION RESULT</span>
|
| 265 |
+
<h1 class="verdict-title" id="verdictTitle">--</h1>
|
| 266 |
+
<div id="detectionBadges" class="detection-badges"></div>
|
| 267 |
+
<div class="confidence-meter">
|
| 268 |
+
<div class="meter-bar">
|
| 269 |
+
<div class="meter-fill" id="confidenceBar"></div>
|
| 270 |
+
</div>
|
| 271 |
+
<span class="meter-value" id="confidenceValue">0%</span>
|
| 272 |
+
</div>
|
| 273 |
+
</div>
|
| 274 |
+
|
| 275 |
+
<!-- Feedback Section -->
|
| 276 |
+
<div class="feedback-section" id="feedbackSection" style="display: none;">
|
| 277 |
+
<div class="feedback-header">
|
| 278 |
+
<h4>Was this prediction correct?</h4>
|
| 279 |
+
<p>Your feedback helps improve the model</p>
|
| 280 |
+
</div>
|
| 281 |
+
<div class="feedback-buttons">
|
| 282 |
+
<button class="btn-feedback btn-feedback-correct" id="btnFeedbackCorrect"
|
| 283 |
+
onclick="submitFeedback(true)">
|
| 284 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
| 285 |
+
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
| 286 |
+
<polyline points="20 6 9 17 4 12"></polyline>
|
| 287 |
+
</svg>
|
| 288 |
+
Prediction is Correct
|
| 289 |
+
</button>
|
| 290 |
+
<button class="btn-feedback btn-feedback-wrong" id="btnFeedbackWrong"
|
| 291 |
+
onclick="submitFeedback(false)">
|
| 292 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
| 293 |
+
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
| 294 |
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
| 295 |
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
| 296 |
+
</svg>
|
| 297 |
+
Prediction is Wrong
|
| 298 |
+
</button>
|
| 299 |
+
</div>
|
| 300 |
+
<div class="feedback-message" id="feedbackMessage" style="display: none;"></div>
|
| 301 |
+
</div>
|
| 302 |
+
|
| 303 |
+
</div>
|
| 304 |
+
|
| 305 |
+
<div class="metrics-grid">
|
| 306 |
+
<!-- Chart Container -->
|
| 307 |
+
<div class="metric-card"
|
| 308 |
+
style="grid-column: span 2; display: flex; justify-content: center; align-items: center; padding: 20px;">
|
| 309 |
+
<canvas id="probabilityChart" style="max-height: 150px;"></canvas>
|
| 310 |
+
</div>
|
| 311 |
+
|
| 312 |
+
<div class="metric-card" style="position: relative; overflow: hidden;">
|
| 313 |
+
<div
|
| 314 |
+
style="position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, var(--accent-yellow), transparent); border-radius: 3px 3px 0 0;">
|
| 315 |
+
</div>
|
| 316 |
+
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
|
| 317 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent-yellow)"
|
| 318 |
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 319 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 320 |
+
<polyline points="12 6 12 12 16 14"></polyline>
|
| 321 |
+
</svg>
|
| 322 |
+
<span class="metric-label" style="margin-bottom: 0;">Scan Time</span>
|
| 323 |
+
</div>
|
| 324 |
+
<span class="metric-value" id="scanTimeDisplay">--</span>
|
| 325 |
+
</div>
|
| 326 |
+
<div class="metric-card" style="position: relative; overflow: hidden;">
|
| 327 |
+
<div
|
| 328 |
+
style="position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, var(--accent-yellow), transparent); border-radius: 3px 3px 0 0;">
|
| 329 |
+
</div>
|
| 330 |
+
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
|
| 331 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent-yellow)"
|
| 332 |
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 333 |
+
<path d="M12 2L2 7l10 5 10-5-10-5z"></path>
|
| 334 |
+
<path d="M2 17l10 5 10-5"></path>
|
| 335 |
+
<path d="M2 12l10 5 10-5"></path>
|
| 336 |
+
</svg>
|
| 337 |
+
<span class="metric-label" style="margin-bottom: 0;">Model Version</span>
|
| 338 |
+
</div>
|
| 339 |
+
<span class="metric-value">Mark V</span>
|
| 340 |
+
</div>
|
| 341 |
+
</div>
|
| 342 |
+
|
| 343 |
+
<div class="analysis-details"
|
| 344 |
+
style="background: rgba(255, 255, 255, 0.03); backdrop-filter: blur(16px); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 20px; padding: 24px; position: relative; overflow: hidden;">
|
| 345 |
+
<div
|
| 346 |
+
style="position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, var(--accent-yellow), transparent);">
|
| 347 |
+
</div>
|
| 348 |
+
<h4
|
| 349 |
+
style="display: flex; align-items: center; gap: 10px; font-family: var(--font-display); margin-bottom: 12px;">
|
| 350 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-yellow)"
|
| 351 |
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 352 |
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
| 353 |
+
<polyline points="14 2 14 8 20 8"></polyline>
|
| 354 |
+
<line x1="16" y1="13" x2="8" y2="13"></line>
|
| 355 |
+
<line x1="16" y1="17" x2="8" y2="17"></line>
|
| 356 |
+
<polyline points="10 9 9 9 8 9"></polyline>
|
| 357 |
+
</svg>
|
| 358 |
+
Forensic Analysis
|
| 359 |
+
</h4>
|
| 360 |
+
<p id="analysisText" style="color: var(--text-secondary); line-height: 1.7;">Waiting for analysis...
|
| 361 |
+
</p>
|
| 362 |
+
|
| 363 |
+
<button class="btn-primary" id="downloadReportBtn"
|
| 364 |
+
style="margin-top: 20px; width: 100%; display: none; position: relative; overflow: hidden;"
|
| 365 |
+
onclick="generatePDFReport()">
|
| 366 |
+
📄 Download Forensic Report
|
| 367 |
+
</button>
|
| 368 |
+
</div>
|
| 369 |
+
</div>
|
| 370 |
+
</div>
|
| 371 |
+
</div>
|
| 372 |
+
|
| 373 |
+
<!-- Statistics Overview -->
|
| 374 |
+
<div class="container" style="max-width: 1200px; margin-top: 60px;" id="statisticsSection">
|
| 375 |
+
<div class="section-header">
|
| 376 |
+
<h2 class="section-title">Analysis <span class="gradient-text">Statistics</span></h2>
|
| 377 |
+
<p class="section-subtitle">Overview of your detection history</p>
|
| 378 |
+
</div>
|
| 379 |
+
<div class="statistics-grid">
|
| 380 |
+
<div class="stat-card"
|
| 381 |
+
style="--accent-color: var(--accent-yellow); animation: fadeInUp 0.5s ease backwards; animation-delay: 0.1s;">
|
| 382 |
+
<div
|
| 383 |
+
style="position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, var(--accent-yellow), transparent); border-radius: 3px 3px 0 0;">
|
| 384 |
+
</div>
|
| 385 |
+
<div class="stat-icon"
|
| 386 |
+
style="background: rgba(227, 245, 20, 0.1); border-radius: 14px; width: 52px; height: 52px; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; border: 1px solid rgba(227, 245, 20, 0.2);">
|
| 387 |
+
📊</div>
|
| 388 |
+
<div class="stat-value" id="totalScans">0</div>
|
| 389 |
+
<div class="stat-label">Total Analyzed</div>
|
| 390 |
+
</div>
|
| 391 |
+
<div class="stat-card"
|
| 392 |
+
style="--accent-color: #ff6b6b; animation: fadeInUp 0.5s ease backwards; animation-delay: 0.2s;">
|
| 393 |
+
<div
|
| 394 |
+
style="position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, #ff6b6b, transparent); border-radius: 3px 3px 0 0;">
|
| 395 |
+
</div>
|
| 396 |
+
<div class="stat-icon"
|
| 397 |
+
style="background: rgba(255, 107, 107, 0.1); border-radius: 14px; width: 52px; height: 52px; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; border: 1px solid rgba(255, 107, 107, 0.2);">
|
| 398 |
+
⚠️</div>
|
| 399 |
+
<div class="stat-value" id="fakeCount">0</div>
|
| 400 |
+
<div class="stat-label">Fake Detected</div>
|
| 401 |
+
</div>
|
| 402 |
+
<div class="stat-card"
|
| 403 |
+
style="--accent-color: #10b981; animation: fadeInUp 0.5s ease backwards; animation-delay: 0.3s;">
|
| 404 |
+
<div
|
| 405 |
+
style="position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, #10b981, transparent); border-radius: 3px 3px 0 0;">
|
| 406 |
+
</div>
|
| 407 |
+
<div class="stat-icon"
|
| 408 |
+
style="background: rgba(16, 185, 129, 0.1); border-radius: 14px; width: 52px; height: 52px; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; border: 1px solid rgba(16, 185, 129, 0.2);">
|
| 409 |
+
✓</div>
|
| 410 |
+
<div class="stat-value" id="realCount">0</div>
|
| 411 |
+
<div class="stat-label">Real Images</div>
|
| 412 |
+
</div>
|
| 413 |
+
<div class="stat-card"
|
| 414 |
+
style="--accent-color: #3b82f6; animation: fadeInUp 0.5s ease backwards; animation-delay: 0.4s;">
|
| 415 |
+
<div
|
| 416 |
+
style="position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, #3b82f6, transparent); border-radius: 3px 3px 0 0;">
|
| 417 |
+
</div>
|
| 418 |
+
<div class="stat-icon"
|
| 419 |
+
style="background: rgba(59, 130, 246, 0.1); border-radius: 14px; width: 52px; height: 52px; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; border: 1px solid rgba(59, 130, 246, 0.2);">
|
| 420 |
+
🎯</div>
|
| 421 |
+
<div class="stat-value" id="avgConfidence">0%</div>
|
| 422 |
+
<div class="stat-label">Avg Confidence</div>
|
| 423 |
+
</div>
|
| 424 |
+
</div>
|
| 425 |
+
</div>
|
| 426 |
+
|
| 427 |
+
<!-- Recent Analyses -->
|
| 428 |
+
<div class="container" style="max-width: 1200px; margin-top: 40px; padding-bottom: 60px;" id="recentSection">
|
| 429 |
+
<div class="section-header">
|
| 430 |
+
<h2 class="section-title">Recent <span class="gradient-text">Analyses</span></h2>
|
| 431 |
+
<a href="history.html" class="btn-secondary" style="font-size: 14px;">View All History →</a>
|
| 432 |
+
</div>
|
| 433 |
+
<div class="recent-grid" id="recentGrid">
|
| 434 |
+
<!-- Recent items will be populated here -->
|
| 435 |
+
</div>
|
| 436 |
+
</div>
|
| 437 |
+
</main>
|
| 438 |
+
|
| 439 |
+
<!-- Footer -->
|
| 440 |
+
<footer class="footer" style="position: relative; z-index: 1;">
|
| 441 |
+
<div class="container">
|
| 442 |
+
<div class="footer-content-minimal">
|
| 443 |
+
<div class="footer-brand">
|
| 444 |
+
<div class="logo">
|
| 445 |
+
<img src="logo.svg" alt="DeepGuard Logo" class="logo-img">
|
| 446 |
+
<span class="logo-text">Deep<span style="color: var(--accent-yellow);">Guard</span></span>
|
| 447 |
+
</div>
|
| 448 |
+
</div>
|
| 449 |
+
<p class="footer-tagline-premium">Protecting digital truth with AI</p>
|
| 450 |
+
<div class="footer-links-premium">
|
| 451 |
+
<a href="https://harshasnade-deepfake-detection.hf.space" target="_blank"
|
| 452 |
+
class="footer-link-premium-item">
|
| 453 |
+
<img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="Hugging Face">
|
| 454 |
+
<span>Models</span>
|
| 455 |
+
</a>
|
| 456 |
+
<a href="https://github.com/Harshvardhan-Asnade/Deepfake-Model" target="_blank"
|
| 457 |
+
class="footer-link-premium-item">
|
| 458 |
+
<svg viewBox="0 0 24 24" fill="currentColor">
|
| 459 |
+
<path
|
| 460 |
+
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
| 461 |
+
</svg>
|
| 462 |
+
<span>GitHub</span>
|
| 463 |
+
</a>
|
| 464 |
+
</div>
|
| 465 |
+
<div class="footer-copyright-premium">
|
| 466 |
+
<p>© 2024 <span style="color: var(--accent-yellow); font-weight: 700;">DeepGuard</span></p>
|
| 467 |
+
</div>
|
| 468 |
+
</div>
|
| 469 |
+
</div>
|
| 470 |
+
<div class="footer-glow-enhanced"></div>
|
| 471 |
+
<div class="footer-glow"></div>
|
| 472 |
+
</footer>
|
| 473 |
+
|
| 474 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 475 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
| 476 |
+
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
|
| 477 |
+
<!-- Toast Container -->
|
| 478 |
+
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
|
| 479 |
+
|
| 480 |
+
<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>
|
| 481 |
+
<script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
|
| 482 |
+
<script src="https://unpkg.com/@studio-freight/lenis@1.0.42/dist/lenis.min.js"></script>
|
| 483 |
+
<script src="motion.js"></script>
|
| 484 |
+
<script src="script.js"></script>
|
| 485 |
+
<script src="mobile.js"></script>
|
| 486 |
+
<script src="pwa.js"></script>
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
</body>
|
| 490 |
+
|
| 491 |
+
</html>
|
frontend/analytics_icon.png
ADDED
|
|
Git LFS Details
|
frontend/animations.css
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ==================== PREMIUM ANIMATIONS ==================== */
|
| 2 |
+
|
| 3 |
+
/* SMOOTH FADE IN UP */
|
| 4 |
+
@keyframes fadeInUp {
|
| 5 |
+
from {
|
| 6 |
+
opacity: 0;
|
| 7 |
+
transform: translate3d(0, 40px, 0);
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
to {
|
| 11 |
+
opacity: 1;
|
| 12 |
+
transform: translate3d(0, 0, 0);
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.animate-fade-up {
|
| 17 |
+
animation: fadeInUp 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
| 18 |
+
opacity: 0;
|
| 19 |
+
/* Init hidden */
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/* STAGGER DELAYS (utility classes) */
|
| 23 |
+
.delay-100 {
|
| 24 |
+
animation-delay: 0.1s;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.delay-200 {
|
| 28 |
+
animation-delay: 0.2s;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.delay-300 {
|
| 32 |
+
animation-delay: 0.3s;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.delay-400 {
|
| 36 |
+
animation-delay: 0.4s;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.delay-500 {
|
| 40 |
+
animation-delay: 0.5s;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* PULSING GLOW (for upload area) */
|
| 44 |
+
@keyframes pulseGlow {
|
| 45 |
+
0% {
|
| 46 |
+
box-shadow: 0 0 0 0 rgba(227, 245, 20, 0.1);
|
| 47 |
+
border-color: rgba(255, 255, 255, 0.1);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
50% {
|
| 51 |
+
box-shadow: 0 0 30px 0 rgba(227, 245, 20, 0.2);
|
| 52 |
+
border-color: rgba(227, 245, 20, 0.5);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
100% {
|
| 56 |
+
box-shadow: 0 0 0 0 rgba(227, 245, 20, 0.1);
|
| 57 |
+
border-color: rgba(255, 255, 255, 0.1);
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.animate-pulse-glow {
|
| 62 |
+
animation: pulseGlow 3s infinite;
|
| 63 |
+
will-change: box-shadow, border-color;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/* SHIMMER BORDER (for premium feel) */
|
| 67 |
+
@keyframes borderShimmer {
|
| 68 |
+
0% {
|
| 69 |
+
background-position: 0% 50%;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
100% {
|
| 73 |
+
background-position: 200% 50%;
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* FLOATING ELEMENT */
|
| 78 |
+
@keyframes floatY {
|
| 79 |
+
|
| 80 |
+
0%,
|
| 81 |
+
100% {
|
| 82 |
+
transform: translateY(0);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
50% {
|
| 86 |
+
transform: translateY(-10px);
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.animate-float {
|
| 91 |
+
animation: floatY 6s ease-in-out infinite;
|
| 92 |
+
will-change: transform;
|
| 93 |
+
/* Hint for GPU promotion */
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/* SCANNER LINE (High Performance - transform based) */
|
| 97 |
+
@keyframes scanLine {
|
| 98 |
+
0% {
|
| 99 |
+
transform: translateY(0%);
|
| 100 |
+
opacity: 0;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
10% {
|
| 104 |
+
opacity: 1;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
90% {
|
| 108 |
+
opacity: 1;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
100% {
|
| 112 |
+
transform: translateY(100%);
|
| 113 |
+
opacity: 0;
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.scanner-line {
|
| 118 |
+
position: absolute;
|
| 119 |
+
top: 0;
|
| 120 |
+
/* Positioned at top, animated via transform */
|
| 121 |
+
left: 0;
|
| 122 |
+
right: 0;
|
| 123 |
+
width: 100%;
|
| 124 |
+
height: 2px;
|
| 125 |
+
background: var(--accent-yellow);
|
| 126 |
+
box-shadow: 0 0 10px var(--accent-yellow);
|
| 127 |
+
animation: scanLine 2s linear infinite;
|
| 128 |
+
z-index: 10;
|
| 129 |
+
pointer-events: none;
|
| 130 |
+
will-change: transform, opacity;
|
| 131 |
+
/* Hint for GPU */
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
/* ==================== ENHANCED PROCESSING OVERLAY ==================== */
|
| 135 |
+
|
| 136 |
+
/* Neural Spinner Animation */
|
| 137 |
+
@keyframes neuralPulse {
|
| 138 |
+
|
| 139 |
+
0%,
|
| 140 |
+
100% {
|
| 141 |
+
transform: scale(1);
|
| 142 |
+
opacity: 0.8;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
50% {
|
| 146 |
+
transform: scale(1.1);
|
| 147 |
+
opacity: 1;
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
@keyframes spinRing {
|
| 152 |
+
0% {
|
| 153 |
+
transform: rotate(0deg);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
100% {
|
| 157 |
+
transform: rotate(360deg);
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.neural-spinner {
|
| 162 |
+
position: relative;
|
| 163 |
+
width: 80px;
|
| 164 |
+
height: 80px;
|
| 165 |
+
margin: 0 auto 30px;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.neural-spinner .spinner-ring {
|
| 169 |
+
position: absolute;
|
| 170 |
+
width: 100%;
|
| 171 |
+
height: 100%;
|
| 172 |
+
border: 3px solid transparent;
|
| 173 |
+
border-top-color: var(--accent-yellow);
|
| 174 |
+
border-radius: 50%;
|
| 175 |
+
animation: spinRing 1.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.neural-spinner .spinner-ring:nth-child(2) {
|
| 179 |
+
width: 70%;
|
| 180 |
+
height: 70%;
|
| 181 |
+
top: 15%;
|
| 182 |
+
left: 15%;
|
| 183 |
+
border-top-color: rgba(227, 245, 20, 0.6);
|
| 184 |
+
animation-duration: 2s;
|
| 185 |
+
animation-direction: reverse;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.neural-spinner .spinner-ring:nth-child(3) {
|
| 189 |
+
width: 50%;
|
| 190 |
+
height: 50%;
|
| 191 |
+
top: 25%;
|
| 192 |
+
left: 25%;
|
| 193 |
+
border-top-color: rgba(227, 245, 20, 0.3);
|
| 194 |
+
animation-duration: 2.5s;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
/* Progress Steps Animation */
|
| 198 |
+
@keyframes stepPulse {
|
| 199 |
+
|
| 200 |
+
0%,
|
| 201 |
+
100% {
|
| 202 |
+
transform: scale(1);
|
| 203 |
+
opacity: 0.5;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
50% {
|
| 207 |
+
transform: scale(1.15);
|
| 208 |
+
opacity: 1;
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
@keyframes stepGlow {
|
| 213 |
+
|
| 214 |
+
0%,
|
| 215 |
+
100% {
|
| 216 |
+
box-shadow: 0 0 10px rgba(227, 245, 20, 0.3);
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
50% {
|
| 220 |
+
box-shadow: 0 0 20px rgba(227, 245, 20, 0.6);
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.progress-steps {
|
| 225 |
+
display: flex;
|
| 226 |
+
justify-content: center;
|
| 227 |
+
gap: 20px;
|
| 228 |
+
margin: 30px 0;
|
| 229 |
+
flex-wrap: wrap;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.progress-step {
|
| 233 |
+
display: flex;
|
| 234 |
+
flex-direction: column;
|
| 235 |
+
align-items: center;
|
| 236 |
+
gap: 8px;
|
| 237 |
+
opacity: 0.4;
|
| 238 |
+
transition: all 0.4s ease;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.progress-step .step-icon {
|
| 242 |
+
width: 50px;
|
| 243 |
+
height: 50px;
|
| 244 |
+
display: flex;
|
| 245 |
+
align-items: center;
|
| 246 |
+
justify-content: center;
|
| 247 |
+
font-size: 24px;
|
| 248 |
+
background: rgba(255, 255, 255, 0.05);
|
| 249 |
+
border: 2px solid rgba(255, 255, 255, 0.1);
|
| 250 |
+
border-radius: 50%;
|
| 251 |
+
transition: all 0.4s ease;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.progress-step .step-label {
|
| 255 |
+
font-size: 12px;
|
| 256 |
+
font-weight: 500;
|
| 257 |
+
color: rgba(255, 255, 255, 0.6);
|
| 258 |
+
text-transform: uppercase;
|
| 259 |
+
letter-spacing: 0.5px;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.progress-step.active {
|
| 263 |
+
opacity: 1;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.progress-step.active .step-icon {
|
| 267 |
+
background: rgba(227, 245, 20, 0.1);
|
| 268 |
+
border-color: var(--accent-yellow);
|
| 269 |
+
animation: stepPulse 2s ease-in-out infinite, stepGlow 2s ease-in-out infinite;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.progress-step.active .step-label {
|
| 273 |
+
color: var(--accent-yellow);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.progress-step.completed {
|
| 277 |
+
opacity: 0.7;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.progress-step.completed .step-icon {
|
| 281 |
+
background: rgba(16, 185, 129, 0.1);
|
| 282 |
+
border-color: #10B981;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.progress-step.completed .step-label {
|
| 286 |
+
color: #10B981;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
/* Processing Overlay Enhanced */
|
| 290 |
+
.processing-overlay-enhanced {
|
| 291 |
+
position: fixed;
|
| 292 |
+
top: 0;
|
| 293 |
+
left: 0;
|
| 294 |
+
right: 0;
|
| 295 |
+
bottom: 0;
|
| 296 |
+
background: rgba(0, 0, 0, 0.95);
|
| 297 |
+
backdrop-filter: blur(10px);
|
| 298 |
+
display: flex;
|
| 299 |
+
align-items: center;
|
| 300 |
+
justify-content: center;
|
| 301 |
+
z-index: 10000;
|
| 302 |
+
padding: 20px;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.processing-content-enhanced {
|
| 306 |
+
max-width: 600px;
|
| 307 |
+
width: 100%;
|
| 308 |
+
text-align: center;
|
| 309 |
+
animation: fadeInUp 0.5s ease-out;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.model-status-badge {
|
| 313 |
+
display: inline-flex;
|
| 314 |
+
align-items: center;
|
| 315 |
+
gap: 8px;
|
| 316 |
+
padding: 8px 16px;
|
| 317 |
+
background: rgba(255, 255, 255, 0.05);
|
| 318 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 319 |
+
border-radius: 20px;
|
| 320 |
+
margin-bottom: 20px;
|
| 321 |
+
font-size: 13px;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.model-status-badge .status-dot {
|
| 325 |
+
width: 8px;
|
| 326 |
+
height: 8px;
|
| 327 |
+
background: var(--accent-yellow);
|
| 328 |
+
border-radius: 50%;
|
| 329 |
+
animation: neuralPulse 2s ease-in-out infinite;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.model-status-badge .status-text {
|
| 333 |
+
color: rgba(255, 255, 255, 0.8);
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
.processing-title-enhanced {
|
| 337 |
+
font-size: 28px;
|
| 338 |
+
font-weight: 700;
|
| 339 |
+
color: #fff;
|
| 340 |
+
margin-bottom: 20px;
|
| 341 |
+
letter-spacing: 0.5px;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.processing-message {
|
| 345 |
+
font-size: 14px;
|
| 346 |
+
color: rgba(255, 255, 255, 0.6);
|
| 347 |
+
margin-top: 20px;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
/* Warm-Up Alert */
|
| 351 |
+
.warmup-alert {
|
| 352 |
+
margin-top: 30px;
|
| 353 |
+
padding: 20px;
|
| 354 |
+
background: rgba(227, 245, 20, 0.05);
|
| 355 |
+
border: 1px solid rgba(227, 245, 20, 0.2);
|
| 356 |
+
border-radius: 12px;
|
| 357 |
+
text-align: left;
|
| 358 |
+
display: flex;
|
| 359 |
+
gap: 15px;
|
| 360 |
+
animation: fadeInUp 0.5s ease-out;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.warmup-icon {
|
| 364 |
+
font-size: 32px;
|
| 365 |
+
flex-shrink: 0;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.warmup-content h4 {
|
| 369 |
+
margin: 0 0 10px 0;
|
| 370 |
+
font-size: 16px;
|
| 371 |
+
font-weight: 600;
|
| 372 |
+
color: var(--accent-yellow);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
.warmup-content p {
|
| 376 |
+
margin: 8px 0;
|
| 377 |
+
font-size: 14px;
|
| 378 |
+
color: rgba(255, 255, 255, 0.8);
|
| 379 |
+
line-height: 1.5;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
.warmup-content .warmup-reason {
|
| 383 |
+
font-size: 13px;
|
| 384 |
+
color: rgba(255, 255, 255, 0.6);
|
| 385 |
+
font-style: italic;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.warmup-content .warmup-reassurance {
|
| 389 |
+
font-size: 13px;
|
| 390 |
+
color: #10B981;
|
| 391 |
+
font-weight: 500;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
/* ==================== ACCESSIBILITY: REDUCED MOTION ==================== */
|
| 395 |
+
@media (prefers-reduced-motion: reduce) {
|
| 396 |
+
|
| 397 |
+
*,
|
| 398 |
+
*::before,
|
| 399 |
+
*::after {
|
| 400 |
+
animation-duration: 0.01ms !important;
|
| 401 |
+
animation-iteration-count: 1 !important;
|
| 402 |
+
transition-duration: 0.01ms !important;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
.animate-fade-up,
|
| 406 |
+
.animate-float,
|
| 407 |
+
.animate-pulse-glow {
|
| 408 |
+
animation: none !important;
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
/* Reduced animations on mobile for performance */
|
| 413 |
+
@media (max-width: 768px) {
|
| 414 |
+
.animate-float {
|
| 415 |
+
animation-duration: 8s;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.animate-pulse-glow {
|
| 419 |
+
animation-duration: 4s;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.progress-steps {
|
| 423 |
+
gap: 12px;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.progress-step .step-icon {
|
| 427 |
+
width: 40px;
|
| 428 |
+
height: 40px;
|
| 429 |
+
font-size: 20px;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
.progress-step .step-label {
|
| 433 |
+
font-size: 10px;
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
.warmup-alert {
|
| 437 |
+
flex-direction: column;
|
| 438 |
+
text-align: center;
|
| 439 |
+
}
|
| 440 |
+
}
|
frontend/assets/demo_part1.mov
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:82193473a0bb0ffb28ec8cdb4543f550ac31acffe94724a48993971ee0c1da7c
|
| 3 |
+
size 31037318
|
frontend/assets/demo_part2.mov
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f2babb244db96caa7b0633c7650139e964471d1556e6fcd89300a211c74faf00
|
| 3 |
+
size 18119552
|
frontend/assets/displacement.png
ADDED
|
Git LFS Details
|
frontend/assets/extension_demo.mov
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6d44b36bc39b1d4fcee78bac3c59d927c9ffef78434488249ba67cf95c0faeea
|
| 3 |
+
size 36407751
|
frontend/assets/gemini_reveal.png
ADDED
|
Git LFS Details
|
frontend/comparison_real.png
ADDED
|
Git LFS Details
|
frontend/config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const CONFIG = {
|
| 2 |
+
// API Base URL - Automatically selects between Localhost and Production
|
| 3 |
+
API_BASE_URL: window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
| 4 |
+
? 'http://localhost:7860'
|
| 5 |
+
: 'https://harshasnade-deepfake-detection.hf.space'
|
| 6 |
+
};
|
frontend/deep_learning_icon.png
ADDED
|
|
Git LFS Details
|
frontend/extension.css
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Extension Section Styles */
|
| 2 |
+
.extension-section {
|
| 3 |
+
padding: 100px 0;
|
| 4 |
+
position: relative;
|
| 5 |
+
background: linear-gradient(180deg, #000 0%, #0a0a0a 100%);
|
| 6 |
+
overflow: hidden;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
.extension-container {
|
| 10 |
+
display: flex;
|
| 11 |
+
align-items: center;
|
| 12 |
+
gap: 60px;
|
| 13 |
+
position: relative;
|
| 14 |
+
z-index: 2;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.extension-content {
|
| 18 |
+
flex: 1;
|
| 19 |
+
text-align: left;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.extension-badge {
|
| 23 |
+
display: inline-flex;
|
| 24 |
+
align-items: center;
|
| 25 |
+
gap: 8px;
|
| 26 |
+
padding: 8px 16px;
|
| 27 |
+
background: rgba(227, 245, 20, 0.1);
|
| 28 |
+
border: 1px solid rgba(227, 245, 20, 0.2);
|
| 29 |
+
border-radius: 100px;
|
| 30 |
+
color: var(--accent-yellow);
|
| 31 |
+
font-size: 14px;
|
| 32 |
+
margin-bottom: 24px;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.extension-title {
|
| 36 |
+
font-size: 48px;
|
| 37 |
+
line-height: 1.1;
|
| 38 |
+
margin-bottom: 20px;
|
| 39 |
+
font-family: 'Space Grotesk', sans-serif;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.extension-description {
|
| 43 |
+
font-size: 18px;
|
| 44 |
+
color: #999;
|
| 45 |
+
margin-bottom: 32px;
|
| 46 |
+
line-height: 1.6;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.chrome-btn {
|
| 50 |
+
display: inline-flex;
|
| 51 |
+
align-items: center;
|
| 52 |
+
gap: 12px;
|
| 53 |
+
background: #fff;
|
| 54 |
+
color: #000;
|
| 55 |
+
padding: 16px 32px;
|
| 56 |
+
border-radius: 12px;
|
| 57 |
+
font-weight: 700;
|
| 58 |
+
font-size: 18px;
|
| 59 |
+
transition: all 0.3s ease;
|
| 60 |
+
text-decoration: none;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.chrome-btn:hover {
|
| 64 |
+
transform: translateY(-2px);
|
| 65 |
+
box-shadow: 0 10px 30px rgba(255, 255, 255, 0.2);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.chrome-icon {
|
| 69 |
+
width: 24px;
|
| 70 |
+
height: 24px;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.extension-visual {
|
| 74 |
+
flex: 1.2;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* Reusing window styles but ensuring specific context */
|
| 78 |
+
.extension-visual .video-window-container {
|
| 79 |
+
box-shadow: -30px 30px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
@media (max-width: 968px) {
|
| 83 |
+
.extension-container {
|
| 84 |
+
flex-direction: column-reverse;
|
| 85 |
+
/* Video on top on mobile? Or text on top? usually text on top. column-reverse puts visual first if html is visual last. */
|
| 86 |
+
flex-direction: column;
|
| 87 |
+
text-align: center;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.extension-content {
|
| 91 |
+
text-align: center;
|
| 92 |
+
}
|
| 93 |
+
}
|
frontend/favicon.ico
ADDED
|
|
Git LFS Details
|
frontend/hero_reveal.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Hero Reveal Effect
|
| 3 |
+
* Adapts the fluid reveal effect for the hero section of the main landing page.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
class HeroFluidReveal {
|
| 7 |
+
constructor() {
|
| 8 |
+
this.container = document.getElementById('heroRevealContainer');
|
| 9 |
+
this.canvas = document.getElementById('revealCanvas');
|
| 10 |
+
|
| 11 |
+
if (!this.container || !this.canvas) {
|
| 12 |
+
console.warn('HeroFluidReveal: Container or Canvas not found.');
|
| 13 |
+
return;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
this.ctx = this.canvas.getContext('2d');
|
| 17 |
+
|
| 18 |
+
// Use the new IDs we added to index.html
|
| 19 |
+
this.bgImg = document.getElementById('hero-img-bg');
|
| 20 |
+
this.revealImg = document.getElementById('hero-img-reveal');
|
| 21 |
+
|
| 22 |
+
// Initialize state
|
| 23 |
+
this.width = this.container.offsetWidth;
|
| 24 |
+
this.height = this.container.offsetHeight;
|
| 25 |
+
|
| 26 |
+
// Mouse state (relative to container)
|
| 27 |
+
this.mouse = { x: this.width / 2, y: this.height / 2 };
|
| 28 |
+
this.targetMouse = { x: this.width / 2, y: this.height / 2 };
|
| 29 |
+
this.isMouseOver = false;
|
| 30 |
+
|
| 31 |
+
// Blob state
|
| 32 |
+
this.blob = {
|
| 33 |
+
x: this.width / 2,
|
| 34 |
+
y: this.height / 2,
|
| 35 |
+
vx: 0,
|
| 36 |
+
vy: 0,
|
| 37 |
+
radius: 250 // Slightly smaller for hero section if needed, or keep 300
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
// Configuration
|
| 41 |
+
this.numPoints = 20;
|
| 42 |
+
this.points = [];
|
| 43 |
+
this.init();
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
init() {
|
| 47 |
+
// Point class definition (same as before)
|
| 48 |
+
this.Point = class {
|
| 49 |
+
constructor(angle, radius) {
|
| 50 |
+
this.angle = angle;
|
| 51 |
+
this.baseRadius = radius;
|
| 52 |
+
this.radius = radius;
|
| 53 |
+
this.x = 0;
|
| 54 |
+
this.y = 0;
|
| 55 |
+
this.noiseOffset = Math.random() * 1000;
|
| 56 |
+
this.speed = 0.002 + Math.random() * 0.003;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
update(centerX, centerY, velocityX, velocityY, time) {
|
| 60 |
+
const noise = Math.sin(time * this.speed + this.noiseOffset) * 20;
|
| 61 |
+
const dirX = Math.cos(this.angle);
|
| 62 |
+
const dirY = Math.sin(this.angle);
|
| 63 |
+
const dot = dirX * velocityX + dirY * velocityY;
|
| 64 |
+
const stretch = dot * 1.5;
|
| 65 |
+
const currentRadius = this.baseRadius + noise - stretch;
|
| 66 |
+
this.x = centerX + Math.cos(this.angle) * currentRadius;
|
| 67 |
+
this.y = centerY + Math.sin(this.angle) * currentRadius;
|
| 68 |
+
}
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
this.resize();
|
| 72 |
+
window.addEventListener('resize', () => this.resize());
|
| 73 |
+
|
| 74 |
+
// Listen to window mouse events to avoid z-index blocking by hero content
|
| 75 |
+
window.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
| 76 |
+
|
| 77 |
+
// Optional: We can still use container bounds to "pause" or hide if needed,
|
| 78 |
+
// but for a background effect, continuous tracking is usually better.
|
| 79 |
+
// Removed container-specific enter/leave to prevent stuttering at edges of children.
|
| 80 |
+
|
| 81 |
+
// Initialize points
|
| 82 |
+
for (let i = 0; i < this.numPoints; i++) {
|
| 83 |
+
const angle = (i / this.numPoints) * Math.PI * 2;
|
| 84 |
+
this.points.push(new this.Point(angle, this.blob.radius));
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// Start loop
|
| 88 |
+
requestAnimationFrame((t) => this.render(t));
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
resize() {
|
| 92 |
+
this.width = this.container.offsetWidth;
|
| 93 |
+
this.height = this.container.offsetHeight;
|
| 94 |
+
this.canvas.width = this.width;
|
| 95 |
+
this.canvas.height = this.height;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
onMouseMove(e) {
|
| 99 |
+
// Calculate mouse position relative to container
|
| 100 |
+
const rect = this.container.getBoundingClientRect();
|
| 101 |
+
this.targetMouse.x = e.clientX - rect.left;
|
| 102 |
+
this.targetMouse.y = e.clientY - rect.top;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
updateBlob() {
|
| 106 |
+
const dx = this.targetMouse.x - this.blob.x;
|
| 107 |
+
const dy = this.targetMouse.y - this.blob.y;
|
| 108 |
+
|
| 109 |
+
// Ease
|
| 110 |
+
const ease = 0.25;
|
| 111 |
+
this.blob.vx += dx * ease;
|
| 112 |
+
this.blob.vy += dy * ease;
|
| 113 |
+
|
| 114 |
+
// Friction
|
| 115 |
+
this.blob.vx *= 0.75;
|
| 116 |
+
this.blob.vy *= 0.75;
|
| 117 |
+
|
| 118 |
+
this.blob.x += this.blob.vx;
|
| 119 |
+
this.blob.y += this.blob.vy;
|
| 120 |
+
|
| 121 |
+
const velX = (this.targetMouse.x - this.blob.x) * 0.1;
|
| 122 |
+
const velY = (this.targetMouse.y - this.blob.y) * 0.1;
|
| 123 |
+
|
| 124 |
+
return { velX, velY };
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
drawBlobPath(time, velX, velY) {
|
| 128 |
+
this.ctx.beginPath();
|
| 129 |
+
this.points.forEach(p => p.update(this.blob.x, this.blob.y, velX, velY, time));
|
| 130 |
+
|
| 131 |
+
const p0 = this.points[0];
|
| 132 |
+
const pLast = this.points[this.points.length - 1];
|
| 133 |
+
const midX = (p0.x + pLast.x) / 2;
|
| 134 |
+
const midY = (p0.y + pLast.y) / 2;
|
| 135 |
+
|
| 136 |
+
this.ctx.moveTo(midX, midY);
|
| 137 |
+
|
| 138 |
+
for (let i = 0; i < this.points.length; i++) {
|
| 139 |
+
const p = this.points[i];
|
| 140 |
+
const nextP = this.points[(i + 1) % this.points.length];
|
| 141 |
+
const nextMidX = (p.x + nextP.x) / 2;
|
| 142 |
+
const nextMidY = (p.y + nextP.y) / 2;
|
| 143 |
+
this.ctx.quadraticCurveTo(p.x, p.y, nextMidX, nextMidY);
|
| 144 |
+
}
|
| 145 |
+
this.ctx.closePath();
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
drawImageCover(img) {
|
| 149 |
+
const imgRatio = img.width / img.height;
|
| 150 |
+
const canvasRatio = this.width / this.height;
|
| 151 |
+
let drawW, drawH, curX, curY;
|
| 152 |
+
|
| 153 |
+
if (imgRatio > canvasRatio) {
|
| 154 |
+
drawH = this.height;
|
| 155 |
+
drawW = drawH * imgRatio;
|
| 156 |
+
curX = (this.width - drawW) / 2;
|
| 157 |
+
curY = 0;
|
| 158 |
+
} else {
|
| 159 |
+
drawW = this.width;
|
| 160 |
+
drawH = drawW / imgRatio;
|
| 161 |
+
curX = 0;
|
| 162 |
+
curY = (this.height - drawH) / 2;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
this.ctx.drawImage(img, curX, curY, drawW, drawH);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
render(time) {
|
| 169 |
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
| 170 |
+
|
| 171 |
+
// IMPORTANT: We do NOT draw the background image on the canvas.
|
| 172 |
+
// The background image is an <img> tag in HTML (id="hero-img-bg").
|
| 173 |
+
// The canvas sits ON TOP of it.
|
| 174 |
+
// The canvas draws the "Reveal" image ONLY inside the blob.
|
| 175 |
+
|
| 176 |
+
// 1. Update Physics
|
| 177 |
+
const velocity = this.updateBlob();
|
| 178 |
+
|
| 179 |
+
// 2. Create Mask and Draw Reveal
|
| 180 |
+
this.ctx.save();
|
| 181 |
+
this.drawBlobPath(time, velocity.velX, velocity.velY);
|
| 182 |
+
this.ctx.clip();
|
| 183 |
+
|
| 184 |
+
// Apply theme-based filter
|
| 185 |
+
const theme = document.documentElement.getAttribute('data-theme');
|
| 186 |
+
if (theme === 'light') {
|
| 187 |
+
// Rotates Yellow (~60deg) to Blue (~240deg) -> +180deg
|
| 188 |
+
this.ctx.filter = 'hue-rotate(190deg) brightness(1.1) saturate(1.2)';
|
| 189 |
+
} else {
|
| 190 |
+
this.ctx.filter = 'none';
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
if (this.revealImg && this.revealImg.complete) {
|
| 194 |
+
this.drawImageCover(this.revealImg);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
// Optional: Add a subtle border/glow to the reveal edge
|
| 198 |
+
// this.ctx.lineWidth = 2;
|
| 199 |
+
// this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
|
| 200 |
+
// this.ctx.stroke();
|
| 201 |
+
|
| 202 |
+
this.ctx.restore();
|
| 203 |
+
|
| 204 |
+
requestAnimationFrame((t) => this.render(t));
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
// Initialize when DOM is ready
|
| 209 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 210 |
+
// Wait slightly to ensure images start loading?
|
| 211 |
+
// Actually window.onload is safer for images but DOMContentLoaded is faster for UI.
|
| 212 |
+
// The class checks .complete so it handles loading.
|
| 213 |
+
new HeroFluidReveal();
|
| 214 |
+
});
|
frontend/history.css
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ==================== PREMIUM HISTORY STYLES ==================== */
|
| 2 |
+
|
| 3 |
+
/* --- Grid System --- */
|
| 4 |
+
.history-grid {
|
| 5 |
+
display: grid;
|
| 6 |
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
| 7 |
+
gap: 24px;
|
| 8 |
+
padding-bottom: 40px;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
/* --- Card Styling (Glass + Neon) --- */
|
| 12 |
+
.history-card,
|
| 13 |
+
.grid-card {
|
| 14 |
+
background: rgba(20, 20, 20, 0.6);
|
| 15 |
+
backdrop-filter: blur(12px);
|
| 16 |
+
-webkit-backdrop-filter: blur(12px);
|
| 17 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 18 |
+
border-radius: 24px;
|
| 19 |
+
padding: 20px;
|
| 20 |
+
display: flex;
|
| 21 |
+
flex-direction: column;
|
| 22 |
+
transition: all 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
|
| 23 |
+
position: relative;
|
| 24 |
+
overflow: hidden;
|
| 25 |
+
animation: fadeInUp 0.6s ease backwards;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.history-card:hover,
|
| 29 |
+
.grid-card:hover {
|
| 30 |
+
transform: translateY(-8px);
|
| 31 |
+
border-color: rgba(227, 245, 20, 0.3);
|
| 32 |
+
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.5), 0 0 20px rgba(227, 245, 20, 0.05);
|
| 33 |
+
background: rgba(255, 255, 255, 0.04);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* Selection State */
|
| 37 |
+
.grid-card.selected {
|
| 38 |
+
border-color: var(--accent-yellow);
|
| 39 |
+
box-shadow: 0 0 0 2px rgba(227, 245, 20, 0.2);
|
| 40 |
+
background: rgba(227, 245, 20, 0.05);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* --- Preview Image --- */
|
| 44 |
+
.grid-preview {
|
| 45 |
+
width: 100%;
|
| 46 |
+
aspect-ratio: 16/9;
|
| 47 |
+
object-fit: cover;
|
| 48 |
+
border-radius: 16px;
|
| 49 |
+
margin-bottom: 16px;
|
| 50 |
+
background: #000;
|
| 51 |
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* --- Badges (Neon Glow) --- */
|
| 55 |
+
.table-badge,
|
| 56 |
+
.history-badge,
|
| 57 |
+
.recent-badge {
|
| 58 |
+
padding: 6px 12px;
|
| 59 |
+
border-radius: 100px;
|
| 60 |
+
font-weight: 700;
|
| 61 |
+
font-size: 11px;
|
| 62 |
+
text-transform: uppercase;
|
| 63 |
+
letter-spacing: 1px;
|
| 64 |
+
display: inline-flex;
|
| 65 |
+
align-items: center;
|
| 66 |
+
gap: 6px;
|
| 67 |
+
backdrop-filter: blur(4px);
|
| 68 |
+
transition: all 0.3s ease;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.fake,
|
| 72 |
+
.badge-fake,
|
| 73 |
+
.verdict-fake {
|
| 74 |
+
background: rgba(227, 245, 20, 0.1);
|
| 75 |
+
color: #E3F514;
|
| 76 |
+
border: 1px solid rgba(227, 245, 20, 0.3);
|
| 77 |
+
box-shadow: 0 0 15px rgba(227, 245, 20, 0.15);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.real,
|
| 81 |
+
.badge-real,
|
| 82 |
+
.verdict-real {
|
| 83 |
+
background: rgba(16, 185, 129, 0.1);
|
| 84 |
+
color: #10B981;
|
| 85 |
+
border: 1px solid rgba(16, 185, 129, 0.3);
|
| 86 |
+
box-shadow: 0 0 15px rgba(16, 185, 129, 0.15);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/* --- Controls Bar (Refined) --- */
|
| 90 |
+
.history-controls {
|
| 91 |
+
display: flex;
|
| 92 |
+
flex-wrap: wrap;
|
| 93 |
+
gap: var(--gap-sm, 16px);
|
| 94 |
+
margin: var(--gap-md, 30px) 0 var(--gap-sm, 24px) 0;
|
| 95 |
+
padding: var(--card-padding, 20px);
|
| 96 |
+
background: rgba(10, 10, 10, 0.6);
|
| 97 |
+
backdrop-filter: blur(16px);
|
| 98 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 99 |
+
border-radius: 20px;
|
| 100 |
+
align-items: center;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.search-container {
|
| 104 |
+
flex: 2;
|
| 105 |
+
min-width: 250px;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.search-input,
|
| 109 |
+
.filter-select {
|
| 110 |
+
width: 100%;
|
| 111 |
+
background: rgba(255, 255, 255, 0.03);
|
| 112 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 113 |
+
border-radius: 12px;
|
| 114 |
+
padding: 12px 18px;
|
| 115 |
+
color: #fff;
|
| 116 |
+
font-size: 14px;
|
| 117 |
+
transition: all 0.3s ease;
|
| 118 |
+
font-family: var(--font-primary);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.search-input:focus,
|
| 122 |
+
.filter-select:focus {
|
| 123 |
+
outline: none;
|
| 124 |
+
border-color: var(--accent-yellow);
|
| 125 |
+
background: rgba(255, 255, 255, 0.06);
|
| 126 |
+
box-shadow: 0 0 15px rgba(227, 245, 20, 0.1);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.filter-controls {
|
| 130 |
+
display: flex;
|
| 131 |
+
gap: 12px;
|
| 132 |
+
flex: 3;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.btn-export,
|
| 136 |
+
.btn-clear-all {
|
| 137 |
+
padding: 12px 20px;
|
| 138 |
+
border-radius: 12px;
|
| 139 |
+
font-weight: 600;
|
| 140 |
+
font-size: 13px;
|
| 141 |
+
cursor: pointer;
|
| 142 |
+
transition: all 0.3s ease;
|
| 143 |
+
border: 1px solid transparent;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.btn-export {
|
| 147 |
+
background: rgba(255, 255, 255, 0.05);
|
| 148 |
+
color: #fff;
|
| 149 |
+
border-color: rgba(255, 255, 255, 0.1);
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.btn-export:hover {
|
| 153 |
+
background: rgba(227, 245, 20, 0.1);
|
| 154 |
+
color: var(--accent-yellow);
|
| 155 |
+
border-color: var(--accent-yellow);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.btn-clear-all {
|
| 159 |
+
background: rgba(255, 59, 48, 0.05);
|
| 160 |
+
color: #ff3b30;
|
| 161 |
+
border-color: rgba(255, 59, 48, 0.2);
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.btn-clear-all:hover {
|
| 165 |
+
background: rgba(255, 59, 48, 0.15);
|
| 166 |
+
border-color: #ff3b30;
|
| 167 |
+
box-shadow: 0 0 15px rgba(255, 59, 48, 0.1);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/* --- Filter Chips --- */
|
| 171 |
+
.filter-chips {
|
| 172 |
+
display: flex;
|
| 173 |
+
gap: 8px;
|
| 174 |
+
width: 100%;
|
| 175 |
+
margin-top: 4px;
|
| 176 |
+
padding-top: 16px;
|
| 177 |
+
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.chip {
|
| 181 |
+
padding: 8px 16px;
|
| 182 |
+
border-radius: 100px;
|
| 183 |
+
background: rgba(255, 255, 255, 0.03);
|
| 184 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 185 |
+
color: #888;
|
| 186 |
+
font-size: 13px;
|
| 187 |
+
font-weight: 500;
|
| 188 |
+
cursor: pointer;
|
| 189 |
+
transition: all 0.3s ease;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.chip:hover {
|
| 193 |
+
background: rgba(255, 255, 255, 0.08);
|
| 194 |
+
color: #fff;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.chip.active {
|
| 198 |
+
background: var(--accent-yellow);
|
| 199 |
+
color: #000;
|
| 200 |
+
border-color: var(--accent-yellow);
|
| 201 |
+
font-weight: 600;
|
| 202 |
+
box-shadow: 0 0 15px rgba(227, 245, 20, 0.3);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
/* --- History Table (Glass) --- */
|
| 206 |
+
.history-table-container {
|
| 207 |
+
background: rgba(10, 10, 10, 0.4);
|
| 208 |
+
backdrop-filter: blur(12px);
|
| 209 |
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 210 |
+
border-radius: 24px;
|
| 211 |
+
overflow: hidden;
|
| 212 |
+
margin-bottom: 60px;
|
| 213 |
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.history-table {
|
| 217 |
+
width: 100%;
|
| 218 |
+
border-collapse: separate;
|
| 219 |
+
border-spacing: 0;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.history-table th {
|
| 223 |
+
background: rgba(255, 255, 255, 0.02);
|
| 224 |
+
padding: 20px 24px;
|
| 225 |
+
text-align: left;
|
| 226 |
+
color: var(--text-secondary);
|
| 227 |
+
font-weight: 600;
|
| 228 |
+
font-size: 12px;
|
| 229 |
+
text-transform: uppercase;
|
| 230 |
+
letter-spacing: 1.5px;
|
| 231 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.history-table td {
|
| 235 |
+
padding: 20px 24px;
|
| 236 |
+
color: #fff;
|
| 237 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
| 238 |
+
vertical-align: middle;
|
| 239 |
+
transition: background 0.2s;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.history-table tbody tr {
|
| 243 |
+
transition: all 0.2s;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.history-table tbody tr:hover {
|
| 247 |
+
background: rgba(255, 255, 255, 0.03);
|
| 248 |
+
transform: scale(1.005);
|
| 249 |
+
/* Subtle scale interaction */
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
/* Table Preview */
|
| 253 |
+
.table-preview-img {
|
| 254 |
+
width: 64px;
|
| 255 |
+
height: 64px;
|
| 256 |
+
object-fit: cover;
|
| 257 |
+
border-radius: 12px;
|
| 258 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 259 |
+
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.table-filename {
|
| 263 |
+
font-weight: 500;
|
| 264 |
+
color: #fff;
|
| 265 |
+
font-family: var(--font-display);
|
| 266 |
+
letter-spacing: 0.5px;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.table-date {
|
| 270 |
+
color: var(--text-secondary);
|
| 271 |
+
font-size: 13px;
|
| 272 |
+
font-variant-numeric: tabular-nums;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
/* Actions in Table */
|
| 276 |
+
.table-actions {
|
| 277 |
+
display: flex;
|
| 278 |
+
gap: 8px;
|
| 279 |
+
opacity: 0.6;
|
| 280 |
+
transition: opacity 0.3s;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.history-table tbody tr:hover .table-actions {
|
| 284 |
+
opacity: 1;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.btn-table-action {
|
| 288 |
+
background: rgba(255, 255, 255, 0.05);
|
| 289 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 290 |
+
color: #fff;
|
| 291 |
+
width: 36px;
|
| 292 |
+
height: 36px;
|
| 293 |
+
padding: 0;
|
| 294 |
+
display: flex;
|
| 295 |
+
align-items: center;
|
| 296 |
+
justify-content: center;
|
| 297 |
+
border-radius: 10px;
|
| 298 |
+
transition: all 0.2s;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.btn-table-action:hover {
|
| 302 |
+
background: var(--accent-yellow);
|
| 303 |
+
color: #000;
|
| 304 |
+
border-color: var(--accent-yellow);
|
| 305 |
+
transform: translateY(-2px);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.btn-table-delete:hover {
|
| 309 |
+
background: #ff3b30;
|
| 310 |
+
color: #fff;
|
| 311 |
+
border-color: #ff3b30;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
/* --- Empty State --- */
|
| 315 |
+
.empty-state {
|
| 316 |
+
padding: 80px 20px;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.empty-icon {
|
| 320 |
+
font-size: 56px;
|
| 321 |
+
margin-bottom: 24px;
|
| 322 |
+
opacity: 0.8;
|
| 323 |
+
filter: drop-shadow(0 0 20px rgba(227, 245, 20, 0.3));
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.empty-state h3 {
|
| 327 |
+
font-size: 24px;
|
| 328 |
+
font-family: var(--font-display);
|
| 329 |
+
margin-bottom: 12px;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
/* --- Pagination (Refined) --- */
|
| 333 |
+
.pagination {
|
| 334 |
+
display: flex;
|
| 335 |
+
justify-content: center;
|
| 336 |
+
align-items: center;
|
| 337 |
+
gap: 20px;
|
| 338 |
+
padding: 30px 0;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.btn-page {
|
| 342 |
+
width: 44px;
|
| 343 |
+
height: 44px;
|
| 344 |
+
border-radius: 12px;
|
| 345 |
+
background: rgba(255, 255, 255, 0.05);
|
| 346 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 347 |
+
color: #fff;
|
| 348 |
+
display: flex;
|
| 349 |
+
align-items: center;
|
| 350 |
+
justify-content: center;
|
| 351 |
+
cursor: pointer;
|
| 352 |
+
transition: all 0.2s;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.btn-page:hover:not(:disabled) {
|
| 356 |
+
background: var(--accent-yellow);
|
| 357 |
+
color: #000;
|
| 358 |
+
border-color: var(--accent-yellow);
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.btn-page:disabled {
|
| 362 |
+
opacity: 0.3;
|
| 363 |
+
cursor: not-allowed;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.page-info {
|
| 367 |
+
font-variant-numeric: tabular-nums;
|
| 368 |
+
color: var(--text-secondary);
|
| 369 |
+
font-weight: 500;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
/* ==================== GRID VIEW STYLES ==================== */
|
| 375 |
+
.history-grid-container {
|
| 376 |
+
display: grid;
|
| 377 |
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
| 378 |
+
gap: 20px;
|
| 379 |
+
margin-bottom: 40px;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
.grid-card {
|
| 383 |
+
background: rgba(17, 17, 17, 0.6);
|
| 384 |
+
backdrop-filter: blur(10px);
|
| 385 |
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 386 |
+
border-radius: 20px;
|
| 387 |
+
overflow: hidden;
|
| 388 |
+
transition: all 0.4s ease;
|
| 389 |
+
position: relative;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
.grid-card:hover {
|
| 393 |
+
transform: translateY(-5px);
|
| 394 |
+
border-color: var(--accent-yellow);
|
| 395 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
.grid-preview {
|
| 399 |
+
width: 100%;
|
| 400 |
+
aspect-ratio: 16/9;
|
| 401 |
+
object-fit: cover;
|
| 402 |
+
cursor: pointer;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
.grid-content {
|
| 406 |
+
padding: 16px;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.grid-header {
|
| 410 |
+
display: flex;
|
| 411 |
+
justify-content: space-between;
|
| 412 |
+
align-items: center;
|
| 413 |
+
margin-bottom: 12px;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.grid-title {
|
| 417 |
+
font-weight: 600;
|
| 418 |
+
font-size: 14px;
|
| 419 |
+
white-space: nowrap;
|
| 420 |
+
overflow: hidden;
|
| 421 |
+
text-overflow: ellipsis;
|
| 422 |
+
margin-bottom: 4px;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.grid-date {
|
| 426 |
+
font-size: 12px;
|
| 427 |
+
color: #666;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
/* ==================== BULK SELECTION ==================== */
|
| 431 |
+
.batch-actions-bar {
|
| 432 |
+
position: fixed;
|
| 433 |
+
bottom: 30px;
|
| 434 |
+
left: 50%;
|
| 435 |
+
transform: translateX(-50%) translateY(100px);
|
| 436 |
+
background: var(--accent-yellow);
|
| 437 |
+
color: #000;
|
| 438 |
+
padding: 12px 24px;
|
| 439 |
+
border-radius: 50px;
|
| 440 |
+
display: flex;
|
| 441 |
+
align-items: center;
|
| 442 |
+
gap: 20px;
|
| 443 |
+
box-shadow: 0 10px 40px rgba(227, 245, 20, 0.3);
|
| 444 |
+
z-index: 1000;
|
| 445 |
+
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.batch-actions-bar.active {
|
| 449 |
+
transform: translateX(-50%) translateY(0);
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.selection-info {
|
| 453 |
+
font-weight: 700;
|
| 454 |
+
font-size: 14px;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
.btn-batch {
|
| 458 |
+
background: rgba(0, 0, 0, 0.1);
|
| 459 |
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
| 460 |
+
padding: 6px 16px;
|
| 461 |
+
border-radius: 20px;
|
| 462 |
+
font-weight: 600;
|
| 463 |
+
font-size: 12px;
|
| 464 |
+
cursor: pointer;
|
| 465 |
+
transition: all 0.2s;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.btn-batch:hover {
|
| 469 |
+
background: rgba(0, 0, 0, 0.2);
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
/* ==================== PAGINATION ==================== */
|
| 473 |
+
.pagination {
|
| 474 |
+
display: flex;
|
| 475 |
+
justify-content: center;
|
| 476 |
+
align-items: center;
|
| 477 |
+
gap: 15px;
|
| 478 |
+
margin-top: 20px;
|
| 479 |
+
padding-bottom: 60px;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
.btn-page {
|
| 483 |
+
background: rgba(255, 255, 255, 0.03);
|
| 484 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 485 |
+
color: #fff;
|
| 486 |
+
width: 36px;
|
| 487 |
+
height: 36px;
|
| 488 |
+
border-radius: 10px;
|
| 489 |
+
display: flex;
|
| 490 |
+
align-items: center;
|
| 491 |
+
justify-content: center;
|
| 492 |
+
cursor: pointer;
|
| 493 |
+
transition: all 0.3s;
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
.btn-page:disabled {
|
| 497 |
+
opacity: 0.3;
|
| 498 |
+
cursor: not-allowed;
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
.btn-page.active {
|
| 502 |
+
background: var(--accent-yellow);
|
| 503 |
+
color: #000;
|
| 504 |
+
border-color: var(--accent-yellow);
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
.page-info {
|
| 508 |
+
color: #888;
|
| 509 |
+
font-size: 14px;
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
/* ==================== PREVIEW MODAL ==================== */
|
| 513 |
+
.modal-overlay {
|
| 514 |
+
position: fixed;
|
| 515 |
+
top: 0;
|
| 516 |
+
left: 0;
|
| 517 |
+
width: 100%;
|
| 518 |
+
height: 100%;
|
| 519 |
+
background: rgba(0, 0, 0, 0.8);
|
| 520 |
+
backdrop-filter: blur(8px);
|
| 521 |
+
z-index: 2000;
|
| 522 |
+
display: none;
|
| 523 |
+
align-items: center;
|
| 524 |
+
justify-content: center;
|
| 525 |
+
padding: 20px;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.modal-container {
|
| 529 |
+
background: #111;
|
| 530 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 531 |
+
border-radius: 24px;
|
| 532 |
+
width: 100%;
|
| 533 |
+
max-width: 900px;
|
| 534 |
+
max-height: 90vh;
|
| 535 |
+
overflow-y: auto;
|
| 536 |
+
position: relative;
|
| 537 |
+
animation: modalSlideUp 0.4s ease;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
@keyframes modalSlideUp {
|
| 541 |
+
from {
|
| 542 |
+
opacity: 0;
|
| 543 |
+
transform: translateY(30px);
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
to {
|
| 547 |
+
opacity: 1;
|
| 548 |
+
transform: translateY(0);
|
| 549 |
+
}
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
.modal-close {
|
| 553 |
+
position: absolute;
|
| 554 |
+
top: 20px;
|
| 555 |
+
right: 20px;
|
| 556 |
+
background: rgba(255, 255, 255, 0.05);
|
| 557 |
+
border: none;
|
| 558 |
+
color: #fff;
|
| 559 |
+
width: 36px;
|
| 560 |
+
height: 36px;
|
| 561 |
+
border-radius: 50%;
|
| 562 |
+
cursor: pointer;
|
| 563 |
+
font-size: 20px;
|
| 564 |
+
z-index: 10;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
.modal-body {
|
| 568 |
+
display: grid;
|
| 569 |
+
grid-template-columns: 1fr 1fr;
|
| 570 |
+
gap: 30px;
|
| 571 |
+
padding: 40px;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
.modal-media {
|
| 575 |
+
width: 100%;
|
| 576 |
+
border-radius: 16px;
|
| 577 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
.modal-details {
|
| 581 |
+
display: flex;
|
| 582 |
+
flex-direction: column;
|
| 583 |
+
gap: 20px;
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
.modal-title {
|
| 587 |
+
font-size: 24px;
|
| 588 |
+
font-weight: 700;
|
| 589 |
+
color: #fff;
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
.notes-section {
|
| 593 |
+
margin-top: 10px;
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
.notes-area {
|
| 597 |
+
width: 100%;
|
| 598 |
+
background: rgba(255, 255, 255, 0.03);
|
| 599 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 600 |
+
border-radius: 12px;
|
| 601 |
+
padding: 12px;
|
| 602 |
+
color: #fff;
|
| 603 |
+
font-size: 14px;
|
| 604 |
+
min-height: 100px;
|
| 605 |
+
resize: vertical;
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
.notes-area:focus {
|
| 609 |
+
outline: none;
|
| 610 |
+
border-color: var(--accent-yellow);
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
.btn-save-notes {
|
| 614 |
+
margin-top: 10px;
|
| 615 |
+
background: var(--accent-yellow);
|
| 616 |
+
color: #000;
|
| 617 |
+
border: none;
|
| 618 |
+
padding: 8px 16px;
|
| 619 |
+
border-radius: 8px;
|
| 620 |
+
font-weight: 600;
|
| 621 |
+
cursor: pointer;
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
@media (max-width: 768px) {
|
| 625 |
+
.modal-body {
|
| 626 |
+
grid-template-columns: 1fr;
|
| 627 |
+
padding: 20px;
|
| 628 |
+
}
|
| 629 |
+
}
|
frontend/history.html
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
| 7 |
+
<title>Scan History - DeepGuard</title>
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link
|
| 11 |
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Grotesk:wght@400;500;600;700&display=swap"
|
| 12 |
+
rel="stylesheet">
|
| 13 |
+
<link rel="stylesheet" href="variables.css">
|
| 14 |
+
<link rel="stylesheet" href="style.css">
|
| 15 |
+
<link rel="stylesheet" href="history.css">
|
| 16 |
+
<link rel="stylesheet" href="animations.css">
|
| 17 |
+
<link rel="stylesheet" href="pwa.css">
|
| 18 |
+
<link rel="stylesheet" href="responsive-additions.css">
|
| 19 |
+
<link rel="stylesheet" href="responsive-pages.css">
|
| 20 |
+
|
| 21 |
+
<!-- PWA Manifest -->
|
| 22 |
+
<link rel="manifest" href="manifest.json">
|
| 23 |
+
<meta name="theme-color" content="#E3F514">
|
| 24 |
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
| 25 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
| 26 |
+
<meta name="apple-mobile-web-app-title" content="DeepGuard">
|
| 27 |
+
<link rel="apple-touch-icon" href="logo.ico">
|
| 28 |
+
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
| 29 |
+
</head>
|
| 30 |
+
|
| 31 |
+
<body class="analysis-page">
|
| 32 |
+
<div class="mesh-background"></div>
|
| 33 |
+
<div id="particles-js"></div>
|
| 34 |
+
|
| 35 |
+
<!-- Navigation -->
|
| 36 |
+
<nav class="navbar">
|
| 37 |
+
<div class="container">
|
| 38 |
+
<div class="nav-content">
|
| 39 |
+
<a href="index.html" class="logo">
|
| 40 |
+
<img src="logo.svg" alt="DeepGuard Logo" class="logo-img">
|
| 41 |
+
<span class="logo-text">Deep<span class="gradient-text">Guard</span></span>
|
| 42 |
+
</a>
|
| 43 |
+
|
| 44 |
+
<!-- Hamburger Menu Button (Mobile) -->
|
| 45 |
+
<button class="hamburger" id="hamburger" aria-label="Toggle navigation menu">
|
| 46 |
+
<span></span>
|
| 47 |
+
<span></span>
|
| 48 |
+
<span></span>
|
| 49 |
+
</button>
|
| 50 |
+
|
| 51 |
+
<!-- Navigation Menu -->
|
| 52 |
+
<div class="nav-menu-wrapper">
|
| 53 |
+
<ul class="nav-menu">
|
| 54 |
+
<li><a href="index.html">Home</a></li>
|
| 55 |
+
<li><a href="analysis.html">Analysis</a></li>
|
| 56 |
+
<li><a href="history.html" class="active">History</a></li>
|
| 57 |
+
</ul>
|
| 58 |
+
<a href="index.html" class="btn-secondary-nav">← Back to Home</a>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
</nav>
|
| 63 |
+
|
| 64 |
+
<main class="analysis-container">
|
| 65 |
+
<div class="container" style="max-width: 1200px; padding-top: 40px;">
|
| 66 |
+
<div class="section-header">
|
| 67 |
+
<h2 class="section-title">Scan <span class="gradient-text">History</span></h2>
|
| 68 |
+
<p class="section-subtitle">View and manage your past detection results</p>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<!-- Controls Bar -->
|
| 72 |
+
<div class="history-controls">
|
| 73 |
+
<div class="search-container">
|
| 74 |
+
<input type="text" id="searchInput" class="search-input" placeholder="🔍 Search by filename..."
|
| 75 |
+
oninput="handleSearch()">
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
<!-- View Toggle -->
|
| 79 |
+
<div class="view-toggle">
|
| 80 |
+
<button class="btn-toggle active" id="listViewBtn" onclick="toggleView('list')" title="List View">
|
| 81 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
| 82 |
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 83 |
+
<line x1="8" y1="6" x2="21" y2="6"></line>
|
| 84 |
+
<line x1="8" y1="12" x2="21" y2="12"></line>
|
| 85 |
+
<line x1="8" y1="18" x2="21" y2="18"></line>
|
| 86 |
+
<line x1="3" y1="6" x2="3.01" y2="6"></line>
|
| 87 |
+
<line x1="3" y1="12" x2="3.01" y2="12"></line>
|
| 88 |
+
<line x1="3" y1="18" x2="3.01" y2="18"></line>
|
| 89 |
+
</svg>
|
| 90 |
+
</button>
|
| 91 |
+
<button class="btn-toggle" id="gridViewBtn" onclick="toggleView('grid')" title="Grid View">
|
| 92 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
| 93 |
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 94 |
+
<rect x="3" y="3" width="7" height="7"></rect>
|
| 95 |
+
<rect x="14" y="3" width="7" height="7"></rect>
|
| 96 |
+
<rect x="14" y="14" width="7" height="7"></rect>
|
| 97 |
+
<rect x="3" y="14" width="7" height="7"></rect>
|
| 98 |
+
</svg>
|
| 99 |
+
</button>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
<div class="filter-controls">
|
| 103 |
+
<select id="filterPrediction" class="filter-select" onchange="applyFilters()">
|
| 104 |
+
<option value="all">All Results</option>
|
| 105 |
+
<option value="FAKE">Fake Only</option>
|
| 106 |
+
<option value="REAL">Real Only</option>
|
| 107 |
+
</select>
|
| 108 |
+
|
| 109 |
+
<select id="filterConfidence" class="filter-select" onchange="applyFilters()">
|
| 110 |
+
<option value="all">All Confidence</option>
|
| 111 |
+
<option value="high">High (>80%)</option>
|
| 112 |
+
<option value="medium">Medium (50-80%)</option>
|
| 113 |
+
<option value="low">Low (<50%)< /option>
|
| 114 |
+
</select>
|
| 115 |
+
|
| 116 |
+
<select id="sortBy" class="filter-select" onchange="applyFilters()">
|
| 117 |
+
<option value="date-desc">Latest First</option>
|
| 118 |
+
<option value="date-asc">Oldest First</option>
|
| 119 |
+
<option value="confidence-desc">Confidence ↓</option>
|
| 120 |
+
<option value="confidence-asc">Confidence ↑</option>
|
| 121 |
+
<option value="filename-asc">Filename A-Z</option>
|
| 122 |
+
</select>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
<div class="export-controls">
|
| 126 |
+
<button class="btn-export" onclick="exportHistory('csv')">
|
| 127 |
+
📄 Export CSV
|
| 128 |
+
</button>
|
| 129 |
+
<button class="btn-export" onclick="exportHistory('json')">
|
| 130 |
+
📋 Export JSON
|
| 131 |
+
</button>
|
| 132 |
+
<button class="btn-clear-all" onclick="confirmClearAll()">
|
| 133 |
+
🗑 Clear All
|
| 134 |
+
</button>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
<!-- Quick Filter Chips -->
|
| 138 |
+
<div class="filter-chips">
|
| 139 |
+
<div class="chip active" onclick="setQuickFilter('all', this)">All SCANS</div>
|
| 140 |
+
<div class="chip" onclick="setQuickFilter('FAKE', this)">FAKES Detected</div>
|
| 141 |
+
<div class="chip" onclick="setQuickFilter('REAL', this)">REAL Media</div>
|
| 142 |
+
<div class="chip" onclick="setQuickFilter('high', this)">High Confidence</div>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
<!-- Results Count -->
|
| 147 |
+
<div class="results-count" id="resultsCount">
|
| 148 |
+
Showing <span id="showingCount">0</span> of <span id="totalCount">0</span> results
|
| 149 |
+
</div>
|
| 150 |
+
|
| 151 |
+
<!-- History Table -->
|
| 152 |
+
<div class="history-table-container" id="historyTableContainer">
|
| 153 |
+
<table class="history-table" id="historyTable">
|
| 154 |
+
<thead>
|
| 155 |
+
<tr>
|
| 156 |
+
<th style="width: 40px; cursor: default;"><input type="checkbox" id="selectAllCheckbox"
|
| 157 |
+
onclick="toggleSelectAll(this)"></th>
|
| 158 |
+
<th>Preview</th>
|
| 159 |
+
<th onclick="sortTable('filename')">Filename <span class="sort-indicator"></span></th>
|
| 160 |
+
<th onclick="sortTable('prediction')">Result <span class="sort-indicator"></span></th>
|
| 161 |
+
<th onclick="sortTable('confidence')">Confidence <span class="sort-indicator"></span></th>
|
| 162 |
+
<th onclick="sortTable('timestamp')">Date <span class="sort-indicator"></span></th>
|
| 163 |
+
<th>Actions</th>
|
| 164 |
+
</tr>
|
| 165 |
+
</thead>
|
| 166 |
+
<tbody id="historyTableBody">
|
| 167 |
+
<!-- Rows will be populated here -->
|
| 168 |
+
</tbody>
|
| 169 |
+
</table>
|
| 170 |
+
|
| 171 |
+
<!-- Empty State -->
|
| 172 |
+
<div class="empty-state" id="historyEmptyState" style="display: none;">
|
| 173 |
+
<div class="empty-icon">📂</div>
|
| 174 |
+
<h3>No History Found</h3>
|
| 175 |
+
<p>Your recent analysis results will appear here.</p>
|
| 176 |
+
<a href="analysis.html" class="btn-mini-corner">
|
| 177 |
+
<i class="fas fa-plus"></i> New Analysis
|
| 178 |
+
</a>
|
| 179 |
+
</div>
|
| 180 |
+
|
| 181 |
+
<!-- No Results State -->
|
| 182 |
+
<div class="empty-state" id="noResultsState" style="display: none;">
|
| 183 |
+
<div class="empty-icon">🔍</div>
|
| 184 |
+
<h3>No Results Found</h3>
|
| 185 |
+
<p>Try adjusting your search or filter criteria.</p>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
<!-- Grid View Container -->
|
| 189 |
+
<div id="historyGridContainer" class="history-grid-container" style="display: none;"></div>
|
| 190 |
+
|
| 191 |
+
<!-- Pagination -->
|
| 192 |
+
<div class="pagination" id="paginationControls">
|
| 193 |
+
<button class="btn-page" id="prevPageBtn" onclick="changePage(-1)" disabled>←</button>
|
| 194 |
+
<span class="page-info">Page <span id="currentPage">1</span> of <span
|
| 195 |
+
id="totalPages">1</span></span>
|
| 196 |
+
<button class="btn-page" id="nextPageBtn" onclick="changePage(1)">→</button>
|
| 197 |
+
</div>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
|
| 201 |
+
<!-- Batch Actions Bar -->
|
| 202 |
+
<div id="batchActionsBar" class="batch-actions-bar">
|
| 203 |
+
<span class="selection-info"><span id="selectedCount">0</span> items selected</span>
|
| 204 |
+
<button class="btn-batch" onclick="batchExport('csv')">Export CSV</button>
|
| 205 |
+
<button class="btn-batch" onclick="batchDelete()" style="color: #ff3b30;">Delete Selected</button>
|
| 206 |
+
<button class="btn-batch" onclick="clearSelection()" style="background: transparent;">Cancel</button>
|
| 207 |
+
</div>
|
| 208 |
+
|
| 209 |
+
<!-- Preview Modal -->
|
| 210 |
+
<div id="previewModal" class="modal-overlay" onclick="closeModal(event)">
|
| 211 |
+
<div class="modal-container">
|
| 212 |
+
<button class="modal-close"
|
| 213 |
+
onclick="document.getElementById('previewModal').style.display = 'none'">×</button>
|
| 214 |
+
<div class="modal-body" id="modalBody">
|
| 215 |
+
<!-- Populated via JS -->
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
</main>
|
| 220 |
+
|
| 221 |
+
<script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
|
| 222 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
| 223 |
+
<script src="https://unpkg.com/@studio-freight/lenis@1.0.42/dist/lenis.min.js"></script>
|
| 224 |
+
<script src="motion.js"></script>
|
| 225 |
+
<script src="config.js"></script>
|
| 226 |
+
<script src="script.js"></script>
|
| 227 |
+
<script src="mobile.js"></script>
|
| 228 |
+
<script src="pwa.js"></script>
|
| 229 |
+
<script>
|
| 230 |
+
// Simple script to handle active state or mock history if we wanted to
|
| 231 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 232 |
+
// We can add mock history here later if requested
|
| 233 |
+
});
|
| 234 |
+
</script>
|
| 235 |
+
</body>
|
| 236 |
+
|
| 237 |
+
</html>
|
frontend/index.html
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
| 7 |
+
<title>DeepGuard - AI-Powered Deepfake Detection System</title>
|
| 8 |
+
<meta name="description"
|
| 9 |
+
content="Advanced AI-powered deepfake detection system using cutting-edge machine learning to identify manipulated media with unprecedented accuracy.">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 11 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 12 |
+
<link
|
| 13 |
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Grotesk:wght@400;500;600;700&display=swap"
|
| 14 |
+
rel="stylesheet">
|
| 15 |
+
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
| 16 |
+
<link rel="stylesheet" href="variables.css">
|
| 17 |
+
<link rel="stylesheet" href="style.css">
|
| 18 |
+
<link rel="stylesheet" href="showcase.css">
|
| 19 |
+
<link rel="stylesheet" href="loader.css">
|
| 20 |
+
<link rel="stylesheet" href="video_player.css">
|
| 21 |
+
<link rel="stylesheet" href="extension.css">
|
| 22 |
+
<link rel="stylesheet" href="scroll_indicator.css">
|
| 23 |
+
<link rel="stylesheet" href="orbit.css">
|
| 24 |
+
<link rel="stylesheet" href="pwa.css">
|
| 25 |
+
<link rel="stylesheet" href="responsive-additions.css">
|
| 26 |
+
|
| 27 |
+
<!-- PWA Manifest -->
|
| 28 |
+
<link rel="manifest" href="manifest.json">
|
| 29 |
+
<meta name="theme-color" content="#E3F514">
|
| 30 |
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
| 31 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
| 32 |
+
<meta name="apple-mobile-web-app-title" content="DeepGuard">
|
| 33 |
+
<link rel="apple-touch-icon" href="logo.ico">
|
| 34 |
+
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
| 35 |
+
</head>
|
| 36 |
+
|
| 37 |
+
<body>
|
| 38 |
+
<!-- Enhanced Loading Screen -->
|
| 39 |
+
<div id="loader-wrapper">
|
| 40 |
+
<!-- Brutalist Counter -->
|
| 41 |
+
<div class="loader-content brutalist">
|
| 42 |
+
<p id="loader-quote">IN A WORLD WHERE SEEING IS NO LONGER BELIEVING,<br>OUR SYSTEM EXISTS TO <span
|
| 43 |
+
class="quote-highlight">PROTECT THE TRUTH</span></p>
|
| 44 |
+
|
| 45 |
+
<!-- Detection Status (Cycling: ANALYZING → REAL? → FAKE?) -->
|
| 46 |
+
<div class="detection-status" id="detectionStatus">
|
| 47 |
+
<span class="status-text">ANALYZING...</span>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<div class="counter-wrapper" style="color: #E3F514; opacity: 1; display: block;">
|
| 52 |
+
<span id="loader-percent">0</span><span class="percent-symbol">%</span>
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
+
<!-- Meta Information -->
|
| 56 |
+
<div class="loader-meta">
|
| 57 |
+
<span>DeepGuard Mark V</span>
|
| 58 |
+
<span id="loaderTimestamp">INITIALIZING SYSTEM</span>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
<!-- Animated Background -->
|
| 62 |
+
<div id="canvas-container"></div>
|
| 63 |
+
<div class="mesh-background"></div>
|
| 64 |
+
<!-- Scroll Progress Bar -->
|
| 65 |
+
<div class="scroll-progress-container">
|
| 66 |
+
<div class="scroll-progress-bar" id="scrollProgress"></div>
|
| 67 |
+
</div>
|
| 68 |
+
<div id="particles-js"></div>
|
| 69 |
+
|
| 70 |
+
<!-- Noise Overlay -->
|
| 71 |
+
<div class="noise-overlay"></div>
|
| 72 |
+
|
| 73 |
+
<!-- Navigation -->
|
| 74 |
+
<nav class="navbar">
|
| 75 |
+
<div class="container">
|
| 76 |
+
<div class="nav-content">
|
| 77 |
+
<a href="index.html" class="logo">
|
| 78 |
+
<img src="logo.svg" alt="DeepGuard Logo" class="logo-img">
|
| 79 |
+
<span class="logo-text">Deep<span style="color: var(--accent-yellow);">Guard</span></span>
|
| 80 |
+
</a>
|
| 81 |
+
|
| 82 |
+
<!-- Hamburger Menu Button (Mobile) -->
|
| 83 |
+
<button class="hamburger" id="hamburger" aria-label="Toggle navigation menu">
|
| 84 |
+
<span></span>
|
| 85 |
+
<span></span>
|
| 86 |
+
<span></span>
|
| 87 |
+
</button>
|
| 88 |
+
|
| 89 |
+
<!-- Navigation Menu -->
|
| 90 |
+
<div class="nav-menu-wrapper">
|
| 91 |
+
<ul class="nav-menu">
|
| 92 |
+
<li><a href="index.html" class="active">Home</a></li>
|
| 93 |
+
<li><a href="analysis.html">Analysis</a></li>
|
| 94 |
+
<li><a href="history.html">History</a></li>
|
| 95 |
+
</ul>
|
| 96 |
+
<a href="analysis.html" class="btn-primary">Get Started</a>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
</nav>
|
| 101 |
+
|
| 102 |
+
<!-- Hero Section -->
|
| 103 |
+
<section class="hero">
|
| 104 |
+
<div class="hero-background">
|
| 105 |
+
<!-- Lottie Player Script -->
|
| 106 |
+
<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>
|
| 107 |
+
|
| 108 |
+
<div class="gradient-orb orb-1"></div>
|
| 109 |
+
<div class="gradient-orb orb-2"></div>
|
| 110 |
+
<div class="gradient-orb orb-3"></div>
|
| 111 |
+
|
| 112 |
+
<!-- Floating 3D Objects -->
|
| 113 |
+
<div class="floating-3d-object floating-cube" id="floatingCube">
|
| 114 |
+
<div class="cube-face cube-front"></div>
|
| 115 |
+
<div class="cube-face cube-back"></div>
|
| 116 |
+
<div class="cube-face cube-right"></div>
|
| 117 |
+
<div class="cube-face cube-left"></div>
|
| 118 |
+
<div class="cube-face cube-top"></div>
|
| 119 |
+
<div class="cube-face cube-bottom"></div>
|
| 120 |
+
</div>
|
| 121 |
+
|
| 122 |
+
<div class="floating-3d-object floating-pyramid" id="floatingPyramid">
|
| 123 |
+
<div class="pyramid-face pyramid-front"></div>
|
| 124 |
+
<div class="pyramid-face pyramid-back"></div>
|
| 125 |
+
<div class="pyramid-face pyramid-left"></div>
|
| 126 |
+
<div class="pyramid-face pyramid-right"></div>
|
| 127 |
+
<div class="pyramid-face pyramid-base"></div>
|
| 128 |
+
</div>
|
| 129 |
+
|
| 130 |
+
<!-- Fluid Hover Reveal Images -->
|
| 131 |
+
<div class="hero-reveal-container" id="heroRevealContainer">
|
| 132 |
+
<!-- Background image removed as per user request -->
|
| 133 |
+
|
| 134 |
+
<img src="assets/gemini_reveal.png" alt="AI Detection Foreground" class="reveal-image reveal-top"
|
| 135 |
+
id="hero-img-reveal" style="display: none;"> <!-- Hidden, drawn by canvas -->
|
| 136 |
+
<canvas id="revealCanvas" class="reveal-canvas"></canvas>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
<div class="container">
|
| 140 |
+
<div class="hero-content">
|
| 141 |
+
<div class="hero-badge">
|
| 142 |
+
<span class="badge-dot"></span>
|
| 143 |
+
<span>AI-Powered Detection</span>
|
| 144 |
+
</div>
|
| 145 |
+
<h1 class="hero-title" data-aos="fade-up" data-aos-delay="100">
|
| 146 |
+
Protect Reality with
|
| 147 |
+
<br />
|
| 148 |
+
<span class="gradient-text-hero">Advanced AI Detection</span>
|
| 149 |
+
</h1>
|
| 150 |
+
<p class="hero-description" data-aos="fade-up" data-aos-delay="200">
|
| 151 |
+
Welcome to the future of media authentication. Our cutting-edge deepfake detection system leverages
|
| 152 |
+
state-of-the-art AI to identify manipulated content with unprecedented accuracy.
|
| 153 |
+
</p>
|
| 154 |
+
<div class="hero-actions" data-aos="fade-up" data-aos-delay="300">
|
| 155 |
+
<a href="analysis.html" class="btn-hero-primary">Try Detection Now</a>
|
| 156 |
+
<a href="#live-demo" class="btn-hero-secondary">
|
| 157 |
+
<span class="play-icon">▶</span>
|
| 158 |
+
Watch Demo
|
| 159 |
+
</a>
|
| 160 |
+
<a href="#extension" class="btn-hero-white">
|
| 161 |
+
<img src="https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg"
|
| 162 |
+
alt="Chrome" width="20" height="20">
|
| 163 |
+
Get Extension
|
| 164 |
+
</a>
|
| 165 |
+
</div>
|
| 166 |
+
<div class="hero-stats" data-aos="zoom-in" data-aos-delay="400" data-speed="0.1">
|
| 167 |
+
<div class="stat-item">
|
| 168 |
+
<div class="stat-value">97%</div>
|
| 169 |
+
<div class="stat-label">Accuracy Rate</div>
|
| 170 |
+
</div>
|
| 171 |
+
<div class="stat-divider"></div>
|
| 172 |
+
<div class="stat-item">
|
| 173 |
+
<div class="stat-value">1.3M</div>
|
| 174 |
+
<div class="stat-label">Dataset Trained On</div>
|
| 175 |
+
</div>
|
| 176 |
+
<div class="stat-divider"></div>
|
| 177 |
+
<div class="stat-item">
|
| 178 |
+
<div class="stat-value">< 2s</div>
|
| 179 |
+
<div class="stat-label">Detection Time</div>
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
</div>
|
| 186 |
+
|
| 187 |
+
<!-- Scroll Indicator (Moved out of hero-content to adhere to bottom of viewport) -->
|
| 188 |
+
<div class="scroll-indicator" data-speed="-0.2">
|
| 189 |
+
<div class="mouse">
|
| 190 |
+
<div class="wheel"></div>
|
| 191 |
+
</div>
|
| 192 |
+
<div class="arrow-scroll"></div>
|
| 193 |
+
</div>
|
| 194 |
+
</section>
|
| 195 |
+
|
| 196 |
+
<!-- Section Divider (Wave) -->
|
| 197 |
+
<div class="section-divider">
|
| 198 |
+
<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 120" preserveAspectRatio="none">
|
| 199 |
+
<path
|
| 200 |
+
d="M321.39,56.44c58-10.79,114.16-30.13,172-41.86,82.39-16.72,168.19-17.73,250.45-.39C823.78,31,906.67,72,985.66,92.83c70.05,18.48,146.53,26.09,214.34,3V0H0V27.35A600.21,600.21,0,0,0,321.39,56.44Z"
|
| 201 |
+
class="shape-fill"></path>
|
| 202 |
+
</svg>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<!-- Live Demo Section -->
|
| 206 |
+
<section id="live-demo" class="video-demo-section" style="padding: 60px 0; position: relative; z-index: 5;">
|
| 207 |
+
<div class="container">
|
| 208 |
+
<div class="section-header">
|
| 209 |
+
<h2 class="section-title">See It In <span class="gradient-text">Action</span></h2>
|
| 210 |
+
<p class="section-subtitle">Watch DeepGuard analyze deepfakes in real-time</p>
|
| 211 |
+
</div>
|
| 212 |
+
|
| 213 |
+
<div class="video-window-container" data-aos="zoom-in-up">
|
| 214 |
+
<!-- Mac Window Header -->
|
| 215 |
+
<div class="window-header">
|
| 216 |
+
<div class="window-controls">
|
| 217 |
+
<span class="control red"></span>
|
| 218 |
+
<span class="control yellow"></span>
|
| 219 |
+
<span class="control green"></span>
|
| 220 |
+
</div>
|
| 221 |
+
<div class="window-title">DeepGuard Live Analysis - Mark V</div>
|
| 222 |
+
</div>
|
| 223 |
+
|
| 224 |
+
<!-- Video Player -->
|
| 225 |
+
<div class="video-content-wrapper">
|
| 226 |
+
<video id="demoVideoPlayer" class="demo-video" muted playsinline preload="metadata" width="100%">
|
| 227 |
+
<source src="assets/demo_part1.mov" type="video/mp4">
|
| 228 |
+
<source src="assets/demo_part1.mov" type="video/quicktime">
|
| 229 |
+
Your browser does not support the video tag.
|
| 230 |
+
</video>
|
| 231 |
+
|
| 232 |
+
<!-- Play Overlay -->
|
| 233 |
+
<div class="play-overlay" id="playOverlay">
|
| 234 |
+
<div class="play-button">▶</div>
|
| 235 |
+
</div>
|
| 236 |
+
|
| 237 |
+
<!-- Progress Bar (Fake OS look) -->
|
| 238 |
+
<div class="video-progress-bar">
|
| 239 |
+
<div class="progress-fill" id="videoProgress"></div>
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
</section>
|
| 245 |
+
|
| 246 |
+
<!-- Extension Showcase Section -->
|
| 247 |
+
<section id="extension" class="extension-section">
|
| 248 |
+
<div class="container">
|
| 249 |
+
<div class="extension-container">
|
| 250 |
+
<div class="extension-content" data-aos="fade-right">
|
| 251 |
+
<div class="extension-badge">
|
| 252 |
+
<span>●</span> Available for Chrome
|
| 253 |
+
</div>
|
| 254 |
+
<h2 class="extension-title">DeepGuard <span class="gradient-text">Everywhere</span></h2>
|
| 255 |
+
<p class="extension-description">
|
| 256 |
+
Detect deepfakes in real-time while you browse social media. Our browser extension automatically
|
| 257 |
+
scans images on X, Instagram, and Reddit.
|
| 258 |
+
</p>
|
| 259 |
+
<a href="https://github.com/Harshvardhan-Asnade/Deepfake-Model/tree/main/extension"
|
| 260 |
+
class="chrome-btn" target="_blank">
|
| 261 |
+
<img src="https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg"
|
| 262 |
+
alt="Chrome" class="chrome-icon">
|
| 263 |
+
Add to Chrome
|
| 264 |
+
</a>
|
| 265 |
+
</div>
|
| 266 |
+
|
| 267 |
+
<div class="extension-visual" data-aos="fade-left">
|
| 268 |
+
<div class="video-window-container" style="max-width: 100%;">
|
| 269 |
+
<div class="window-header">
|
| 270 |
+
<div class="window-controls">
|
| 271 |
+
<span class="control red"></span>
|
| 272 |
+
<span class="control yellow"></span>
|
| 273 |
+
<span class="control green"></span>
|
| 274 |
+
</div>
|
| 275 |
+
<div class="window-title">DeepGuard Extension Preview</div>
|
| 276 |
+
</div>
|
| 277 |
+
<div class="video-content-wrapper">
|
| 278 |
+
<video class="demo-video" autoplay muted loop playsinline preload="metadata" width="100%">
|
| 279 |
+
<source src="assets/extension_demo.mov" type="video/mp4">
|
| 280 |
+
<source src="assets/extension_demo.mov" type="video/quicktime">
|
| 281 |
+
</video>
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
</div>
|
| 285 |
+
</div>
|
| 286 |
+
</div>
|
| 287 |
+
</section>
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
<!-- Technology Stack Section -->
|
| 292 |
+
<section id="technology" class="tech-section">
|
| 293 |
+
<div class="container">
|
| 294 |
+
<div class="section-header">
|
| 295 |
+
<h2 class="section-title">Built with <span class="gradient-text">Advanced Technology</span></h2>
|
| 296 |
+
<p class="section-subtitle">Enterprise-grade AI infrastructure powering reliable detection</p>
|
| 297 |
+
</div>
|
| 298 |
+
<div class="tech-grid">
|
| 299 |
+
<div class="tech-card" data-aos="zoom-in" data-aos-delay="100">
|
| 300 |
+
<div class="tech-icon">🧠</div>
|
| 301 |
+
<h3>EfficientNet V2</h3>
|
| 302 |
+
<p>Spatial Feature Extraction</p>
|
| 303 |
+
</div>
|
| 304 |
+
<div class="tech-card" data-aos="zoom-in" data-aos-delay="200">
|
| 305 |
+
<div class="tech-icon">🌪️</div>
|
| 306 |
+
<h3>Swin Transformer</h3>
|
| 307 |
+
<p>Global Context Attention</p>
|
| 308 |
+
</div>
|
| 309 |
+
<div class="tech-card" data-aos="zoom-in" data-aos-delay="300">
|
| 310 |
+
<div class="tech-icon">🌊</div>
|
| 311 |
+
<h3>FFT Analysis</h3>
|
| 312 |
+
<p>Frequency Domain Inspection</p>
|
| 313 |
+
</div>
|
| 314 |
+
<div class="tech-card" data-aos="zoom-in" data-aos-delay="400">
|
| 315 |
+
<div class="tech-icon">🐍</div>
|
| 316 |
+
<h3>Python & PyTorch</h3>
|
| 317 |
+
<p>Core AI Framework</p>
|
| 318 |
+
</div>
|
| 319 |
+
<div class="tech-card" data-aos="zoom-in" data-aos-delay="500">
|
| 320 |
+
<div class="tech-icon">🎯</div>
|
| 321 |
+
<h3>Patch Encoder</h3>
|
| 322 |
+
<p>Local Artifact Detection</p>
|
| 323 |
+
</div>
|
| 324 |
+
</div>
|
| 325 |
+
</div>
|
| 326 |
+
</section>
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
<!-- How It Works -->
|
| 333 |
+
<section class="how-it-works">
|
| 334 |
+
<div class="container">
|
| 335 |
+
<div class="section-header">
|
| 336 |
+
<h2 class="section-title">How It <span class="gradient-text">Works</span></h2>
|
| 337 |
+
<p class="section-subtitle">Advanced AI pipeline for accurate deepfake detection</p>
|
| 338 |
+
</div>
|
| 339 |
+
<div class="pipeline">
|
| 340 |
+
<div class="pipeline-step">
|
| 341 |
+
<div class="step-number">01</div>
|
| 342 |
+
<div class="step-icon">📤</div>
|
| 343 |
+
<h3 class="step-title">Upload Media</h3>
|
| 344 |
+
<p class="step-description">Upload your image or video file through our secure platform</p>
|
| 345 |
+
</div>
|
| 346 |
+
<div class="pipeline-arrow">→</div>
|
| 347 |
+
<div class="pipeline-step">
|
| 348 |
+
<div class="step-number">02</div>
|
| 349 |
+
<div class="step-icon">🔍</div>
|
| 350 |
+
<h3 class="step-title">AI Analysis</h3>
|
| 351 |
+
<p class="step-description">Deep learning model analyzes pixel patterns and artifacts</p>
|
| 352 |
+
</div>
|
| 353 |
+
<div class="pipeline-arrow">→</div>
|
| 354 |
+
<div class="pipeline-step">
|
| 355 |
+
<div class="step-number">03</div>
|
| 356 |
+
<div class="step-icon">🧮</div>
|
| 357 |
+
<h3 class="step-title">Multi-Modal Fusion</h3>
|
| 358 |
+
<p class="step-description">Analyzes frequency (FFT), local patches, and global context
|
| 359 |
+
simultaneously</p>
|
| 360 |
+
</div>
|
| 361 |
+
<div class="pipeline-arrow">→</div>
|
| 362 |
+
<div class="pipeline-step">
|
| 363 |
+
<div class="step-number">04</div>
|
| 364 |
+
<div class="step-icon">✨</div>
|
| 365 |
+
<h3 class="step-title">Results</h3>
|
| 366 |
+
<p class="step-description">Receive detailed report with confidence score and analysis</p>
|
| 367 |
+
</div>
|
| 368 |
+
</div>
|
| 369 |
+
</div>
|
| 370 |
+
</section>
|
| 371 |
+
|
| 372 |
+
<!-- Live Demo Section -->
|
| 373 |
+
<!-- Demo Section Removed - Moved to top -->
|
| 374 |
+
|
| 375 |
+
<!-- CTA Section -->
|
| 376 |
+
<section class="cta-section">
|
| 377 |
+
<div class="container">
|
| 378 |
+
<div class="cta-content">
|
| 379 |
+
<h2 class="cta-title">Ready to Protect Against Deepfakes?</h2>
|
| 380 |
+
<p class="cta-description">Join thousands of users trusting our AI-powered detection system</p>
|
| 381 |
+
<div class="cta-actions">
|
| 382 |
+
<a href="analysis.html" class="btn-cta-primary">Get Started Free</a>
|
| 383 |
+
|
| 384 |
+
</div>
|
| 385 |
+
</div>
|
| 386 |
+
</div>
|
| 387 |
+
</section>
|
| 388 |
+
|
| 389 |
+
<!-- Footer -->
|
| 390 |
+
<footer class="footer">
|
| 391 |
+
<div class="container">
|
| 392 |
+
<div class="footer-content-minimal">
|
| 393 |
+
<!-- Brand -->
|
| 394 |
+
<div class="footer-brand">
|
| 395 |
+
<div class="logo">
|
| 396 |
+
<img src="logo.svg" alt="DeepGuard Logo" class="logo-img">
|
| 397 |
+
<span class="logo-text">Deep<span style="color: var(--accent-yellow);">Guard</span></span>
|
| 398 |
+
</div>
|
| 399 |
+
</div>
|
| 400 |
+
|
| 401 |
+
<!-- Tagline -->
|
| 402 |
+
<p class="footer-tagline-premium">Protecting digital truth with AI</p>
|
| 403 |
+
|
| 404 |
+
<!-- Links -->
|
| 405 |
+
<div class="footer-links-premium">
|
| 406 |
+
<a href="https://harshasnade-deepfake-detection.hf.space" target="_blank"
|
| 407 |
+
class="footer-link-premium-item">
|
| 408 |
+
<img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="Hugging Face">
|
| 409 |
+
<span>Models</span>
|
| 410 |
+
</a>
|
| 411 |
+
<a href="https://github.com/Harshvardhan-Asnade/Deepfake-Model" target="_blank"
|
| 412 |
+
class="footer-link-premium-item">
|
| 413 |
+
<svg viewBox="0 0 24 24" fill="currentColor">
|
| 414 |
+
<path
|
| 415 |
+
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
| 416 |
+
</svg>
|
| 417 |
+
<span>GitHub</span>
|
| 418 |
+
</a>
|
| 419 |
+
</div>
|
| 420 |
+
|
| 421 |
+
<!-- Copyright -->
|
| 422 |
+
<div class="footer-copyright-premium">
|
| 423 |
+
<p>© 2024 <span style="color: var(--accent-yellow); font-weight: 700;">DeepGuard</span></p>
|
| 424 |
+
</div>
|
| 425 |
+
</div>
|
| 426 |
+
</div>
|
| 427 |
+
|
| 428 |
+
<!-- Enhanced decorative glows -->
|
| 429 |
+
<div class="footer-glow-enhanced"></div>
|
| 430 |
+
<div class="footer-glow"></div>
|
| 431 |
+
</footer>
|
| 432 |
+
|
| 433 |
+
<script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
|
| 434 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 435 |
+
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
|
| 436 |
+
<script src="three_bg.js"></script>
|
| 437 |
+
<script src="loader.js"></script>
|
| 438 |
+
<script src="script.js"></script>
|
| 439 |
+
<script src="hero_reveal.js"></script>
|
| 440 |
+
<script src="https://unpkg.com/@studio-freight/lenis@1.0.42/dist/lenis.min.js"></script>
|
| 441 |
+
<script src="motion.js"></script>
|
| 442 |
+
<script src="orbit_interaction.js"></script>
|
| 443 |
+
<script src="mobile.js"></script>
|
| 444 |
+
<script src="pwa.js"></script>
|
| 445 |
+
</body>
|
| 446 |
+
|
| 447 |
+
</html>
|
frontend/loader.css
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ==================== BRUTALIST LOADER ==================== */
|
| 2 |
+
#loader-wrapper {
|
| 3 |
+
position: fixed;
|
| 4 |
+
top: 0;
|
| 5 |
+
left: 0;
|
| 6 |
+
width: 100vw;
|
| 7 |
+
height: 100vh;
|
| 8 |
+
background-color: #000000 !important;
|
| 9 |
+
z-index: 2147483647 !important;
|
| 10 |
+
/* Max Z-Index */
|
| 11 |
+
display: flex;
|
| 12 |
+
flex-direction: column;
|
| 13 |
+
justify-content: center;
|
| 14 |
+
align-items: center;
|
| 15 |
+
overflow: hidden;
|
| 16 |
+
color: #ffffff;
|
| 17 |
+
font-family: sans-serif;
|
| 18 |
+
/* Fallback first */
|
| 19 |
+
transition: transform 0.5s ease-in-out;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
#loader-wrapper.loaded {
|
| 23 |
+
transform: translateY(-100%);
|
| 24 |
+
pointer-events: none;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/* Brutalist Content */
|
| 28 |
+
.loader-content {
|
| 29 |
+
text-align: center;
|
| 30 |
+
position: relative;
|
| 31 |
+
z-index: 2147483647;
|
| 32 |
+
width: 100%;
|
| 33 |
+
display: flex;
|
| 34 |
+
flex-direction: column;
|
| 35 |
+
align-items: center;
|
| 36 |
+
justify-content: center;
|
| 37 |
+
opacity: 1 !important;
|
| 38 |
+
visibility: visible !important;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.counter-wrapper {
|
| 42 |
+
font-size: 10vw;
|
| 43 |
+
font-weight: 900;
|
| 44 |
+
font-family: sans-serif;
|
| 45 |
+
line-height: 1;
|
| 46 |
+
color: #E3F514 !important;
|
| 47 |
+
/* Force Yellow */
|
| 48 |
+
position: absolute;
|
| 49 |
+
bottom: 40px;
|
| 50 |
+
right: 40px;
|
| 51 |
+
margin: 0;
|
| 52 |
+
display: block;
|
| 53 |
+
opacity: 1 !important;
|
| 54 |
+
text-align: right;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
#loader-quote {
|
| 58 |
+
color: #ffffff;
|
| 59 |
+
font-family: 'Space Grotesk', sans-serif;
|
| 60 |
+
font-size: 2.5rem;
|
| 61 |
+
font-weight: 700;
|
| 62 |
+
text-align: center;
|
| 63 |
+
max-width: 90%;
|
| 64 |
+
margin-bottom: 2rem;
|
| 65 |
+
opacity: 1;
|
| 66 |
+
line-height: 1.3;
|
| 67 |
+
text-transform: uppercase;
|
| 68 |
+
letter-spacing: 0.1em;
|
| 69 |
+
z-index: 2147483648;
|
| 70 |
+
|
| 71 |
+
padding: 0;
|
| 72 |
+
border: none;
|
| 73 |
+
box-shadow: none;
|
| 74 |
+
backdrop-filter: none;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* Character States for Monkeytype effect (DeepGuard Themed) */
|
| 78 |
+
.char-waiting {
|
| 79 |
+
color: rgba(255, 255, 255, 0.2);
|
| 80 |
+
transition: color 0.1s ease;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.char-typed {
|
| 84 |
+
color: #ffffff;
|
| 85 |
+
text-shadow: 0 0 15px rgba(255, 255, 255, 0.3);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.char-current {
|
| 89 |
+
background-color: #E3F514;
|
| 90 |
+
color: #000000;
|
| 91 |
+
border-radius: 0px;
|
| 92 |
+
/* Brutalist sharp edges */
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.quote-highlight {
|
| 96 |
+
/* Reset glass effect to allow inner spans to control color */
|
| 97 |
+
background: none;
|
| 98 |
+
-webkit-text-fill-color: initial;
|
| 99 |
+
text-fill-color: initial;
|
| 100 |
+
font-weight: inherit;
|
| 101 |
+
letter-spacing: inherit;
|
| 102 |
+
filter: none;
|
| 103 |
+
display: inline;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/* Remove underline for pure glass text look */
|
| 107 |
+
.quote-highlight::after {
|
| 108 |
+
display: none;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
#loader-percent {
|
| 112 |
+
color: #E3F514 !important;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.percent-symbol {
|
| 116 |
+
font-size: 4vw;
|
| 117 |
+
vertical-align: super;
|
| 118 |
+
margin-left: 10px;
|
| 119 |
+
opacity: 1 !important;
|
| 120 |
+
color: #E3F514 !important;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/* Status Text */
|
| 124 |
+
.detection-status {
|
| 125 |
+
font-family: monospace, sans-serif;
|
| 126 |
+
font-size: 1.5rem;
|
| 127 |
+
letter-spacing: 0.1em;
|
| 128 |
+
text-transform: uppercase;
|
| 129 |
+
font-weight: bold;
|
| 130 |
+
min-height: 2em;
|
| 131 |
+
display: flex;
|
| 132 |
+
justify-content: center;
|
| 133 |
+
align-items: center;
|
| 134 |
+
color: #ffffff !important;
|
| 135 |
+
margin-bottom: 20px;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.status-text {
|
| 139 |
+
color: #ffffff !important;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
/* Meta Information */
|
| 143 |
+
.loader-meta {
|
| 144 |
+
position: absolute;
|
| 145 |
+
bottom: 40px;
|
| 146 |
+
width: 100%;
|
| 147 |
+
text-align: center;
|
| 148 |
+
color: #666;
|
| 149 |
+
font-size: 14px;
|
| 150 |
+
z-index: 2147483647;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/* Mobile Responsive */
|
| 154 |
+
@media (max-width: 768px) {
|
| 155 |
+
.counter-wrapper {
|
| 156 |
+
font-size: 15vw;
|
| 157 |
+
bottom: 20px;
|
| 158 |
+
right: 20px;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
#loader-quote {
|
| 162 |
+
font-size: 1.4rem;
|
| 163 |
+
width: 90%;
|
| 164 |
+
padding: 1.5rem;
|
| 165 |
+
letter-spacing: 0.05em;
|
| 166 |
+
}
|
| 167 |
+
}
|
frontend/loader.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ==================== ENHANCED LOADER SYSTEM ====================
|
| 2 |
+
// Countdown Timer and Real/Fake Status Cycling (Minimum 5 seconds)
|
| 3 |
+
|
| 4 |
+
const initLoader = () => {
|
| 5 |
+
console.log("Initializing Loader...");
|
| 6 |
+
const loaderWrapper = document.getElementById('loader-wrapper');
|
| 7 |
+
const loaderPercent = document.getElementById('loader-percent');
|
| 8 |
+
const detectionStatus = document.getElementById('detectionStatus');
|
| 9 |
+
const statusText = detectionStatus?.querySelector('.status-text');
|
| 10 |
+
const loaderTimestamp = document.getElementById('loaderTimestamp');
|
| 11 |
+
const loaderQuote = document.getElementById('loader-quote');
|
| 12 |
+
|
| 13 |
+
// Global flag for backend readiness (default false)
|
| 14 |
+
window.isBackendReady = false;
|
| 15 |
+
|
| 16 |
+
// Expose status update function
|
| 17 |
+
window.updateLoaderStatus = (message) => {
|
| 18 |
+
if (statusText) {
|
| 19 |
+
statusText.textContent = message;
|
| 20 |
+
// Clear animations/colors to show this is a sticky state
|
| 21 |
+
if (detectionStatus) {
|
| 22 |
+
detectionStatus.classList.remove('analyzing', 'real', 'fake');
|
| 23 |
+
detectionStatus.classList.add('analyzing');
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
if (!loaderWrapper) {
|
| 29 |
+
console.error("Loader wrapper not found!");
|
| 30 |
+
return;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Force visibility at start
|
| 34 |
+
loaderWrapper.style.display = 'flex';
|
| 35 |
+
loaderWrapper.style.opacity = '1';
|
| 36 |
+
|
| 37 |
+
// Prevent double initialization
|
| 38 |
+
if (loaderWrapper.dataset.initialized) return;
|
| 39 |
+
loaderWrapper.dataset.initialized = "true";
|
| 40 |
+
|
| 41 |
+
let progress = 0;
|
| 42 |
+
const targetProgress = 100;
|
| 43 |
+
let currentStatus = 0;
|
| 44 |
+
const startTime = Date.now();
|
| 45 |
+
const minimumDuration = 3500; // 3.5 seconds (Optimized for better UX)
|
| 46 |
+
|
| 47 |
+
// Safety Timeout - Force remove loader after 2 minutes (120000ms) to allow for cold starts
|
| 48 |
+
// If backend is completely dead, this ensures user isn't stuck forever.
|
| 49 |
+
setTimeout(() => {
|
| 50 |
+
if (loaderWrapper && loaderWrapper.style.display !== 'none' && !loaderWrapper.classList.contains('loaded')) {
|
| 51 |
+
console.warn("Loader safety timeout triggered - forcing removal.");
|
| 52 |
+
loaderWrapper.classList.add('loaded');
|
| 53 |
+
setTimeout(() => {
|
| 54 |
+
loaderWrapper.style.display = 'none';
|
| 55 |
+
}, 800);
|
| 56 |
+
}
|
| 57 |
+
}, 120000);
|
| 58 |
+
|
| 59 |
+
// Real/Fake Status Messages
|
| 60 |
+
const statusMessages = [
|
| 61 |
+
{ text: 'ANALYZING...', class: 'analyzing' },
|
| 62 |
+
{ text: 'SCANNING PATTERNS...', class: 'analyzing' },
|
| 63 |
+
{ text: 'REAL?', class: 'real' },
|
| 64 |
+
{ text: 'CHECKING AUTHENTICITY...', class: 'analyzing' },
|
| 65 |
+
{ text: 'FAKE?', class: 'fake' },
|
| 66 |
+
{ text: 'VERIFYING DATA...', class: 'analyzing' }
|
| 67 |
+
];
|
| 68 |
+
|
| 69 |
+
// Update timestamp
|
| 70 |
+
const updateTimestamp = () => {
|
| 71 |
+
const now = new Date();
|
| 72 |
+
const timeStr = now.toLocaleTimeString('en-US', { hour12: false });
|
| 73 |
+
if (loaderTimestamp) {
|
| 74 |
+
loaderTimestamp.textContent = `SYSTEM TIME: ${timeStr}`;
|
| 75 |
+
}
|
| 76 |
+
};
|
| 77 |
+
updateTimestamp();
|
| 78 |
+
const timeInterval = setInterval(updateTimestamp, 1000);
|
| 79 |
+
|
| 80 |
+
// Cycle through status messages
|
| 81 |
+
let statusInterval;
|
| 82 |
+
const cycleStatus = () => {
|
| 83 |
+
// If external system says we are waiting (via explicit message override), stop cycling
|
| 84 |
+
// We'll use a property on the statusText or just check text content if it matches our "Starting..." message?
|
| 85 |
+
// Simpler: Just rely on the animation loop. If we are paused at 99%, we stop cycling.
|
| 86 |
+
|
| 87 |
+
if (!statusText || !detectionStatus) return;
|
| 88 |
+
|
| 89 |
+
// If we are waiting for backend (progress capped at 99), stop cycling status text
|
| 90 |
+
if (progress >= 99 && !window.isBackendReady) {
|
| 91 |
+
return;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
const status = statusMessages[currentStatus];
|
| 95 |
+
statusText.textContent = status.text;
|
| 96 |
+
|
| 97 |
+
// Remove all status classes
|
| 98 |
+
detectionStatus.classList.remove('analyzing', 'real', 'fake');
|
| 99 |
+
// Add current class
|
| 100 |
+
detectionStatus.classList.add(status.class);
|
| 101 |
+
|
| 102 |
+
currentStatus = (currentStatus + 1) % statusMessages.length;
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
// Start cycling status every 800ms
|
| 106 |
+
cycleStatus();
|
| 107 |
+
statusInterval = setInterval(cycleStatus, 800);
|
| 108 |
+
|
| 109 |
+
// Countdown Timer Animation - Smooth progression from 0 to 100
|
| 110 |
+
let lastFrameTime = startTime;
|
| 111 |
+
|
| 112 |
+
// Pre-process loader quotes for typing effect
|
| 113 |
+
let allChars = [];
|
| 114 |
+
if (loaderQuote && !loaderQuote.dataset.processed) {
|
| 115 |
+
loaderQuote.dataset.processed = "true";
|
| 116 |
+
|
| 117 |
+
const processNode = (node) => {
|
| 118 |
+
if (node.nodeType === Node.TEXT_NODE) {
|
| 119 |
+
const text = node.textContent;
|
| 120 |
+
// Skip empty text nodes that are just whitespace to avoid weird spacing gaps if flex/grid were used,
|
| 121 |
+
// but for standard flow, whitespace is needed. However, large blocks of whitespace can be ignored.
|
| 122 |
+
if (text.trim().length === 0 && text.length > 0) {
|
| 123 |
+
// Keep the whitespace node as is
|
| 124 |
+
return;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
const fragment = document.createDocumentFragment();
|
| 128 |
+
const map = text.split('');
|
| 129 |
+
map.forEach(char => {
|
| 130 |
+
const span = document.createElement('span');
|
| 131 |
+
span.textContent = char;
|
| 132 |
+
span.className = 'char-waiting'; // Start in waiting state
|
| 133 |
+
fragment.appendChild(span);
|
| 134 |
+
allChars.push(span);
|
| 135 |
+
});
|
| 136 |
+
node.replaceWith(fragment);
|
| 137 |
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
| 138 |
+
if (node.tagName !== 'BR') {
|
| 139 |
+
Array.from(node.childNodes).forEach(processNode);
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
};
|
| 143 |
+
|
| 144 |
+
Array.from(loaderQuote.childNodes).forEach(processNode);
|
| 145 |
+
|
| 146 |
+
// Reveal the quote container after processing spans
|
| 147 |
+
// Synchronous update to prevent frame flicker
|
| 148 |
+
loaderQuote.style.opacity = '1';
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
function animateLoader() {
|
| 152 |
+
const now = Date.now();
|
| 153 |
+
const elapsed = now - startTime;
|
| 154 |
+
|
| 155 |
+
// Calculate exact progress based on time (0 to 100 over 5 seconds)
|
| 156 |
+
let exactProgress = Math.min((elapsed / minimumDuration) * 100, 100);
|
| 157 |
+
|
| 158 |
+
// BLOCKING: If backend is not ready, cap progress at 99%
|
| 159 |
+
if (!window.isBackendReady && exactProgress >= 99) {
|
| 160 |
+
// TIMEOUT FALLBACK: If we've been waiting too long (> 8 seconds), just let them in.
|
| 161 |
+
if (elapsed > 8000) {
|
| 162 |
+
console.warn("Backend check timed out - proceeding anyway.");
|
| 163 |
+
window.isBackendReady = true;
|
| 164 |
+
} else {
|
| 165 |
+
exactProgress = 99;
|
| 166 |
+
// Update status text if it hasn't been updated yet to show waiting state
|
| 167 |
+
if (statusText && statusText.textContent !== "STARTING SERVER..." && statusText.textContent !== "WAITING FOR BACKEND...") {
|
| 168 |
+
// We rely on script.js calling updateLoaderStatus, but we can also set a default here if stuck
|
| 169 |
+
// But let's let script.js drive the specific message
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// Update progress value directly (no interpolation to avoid jumps)
|
| 175 |
+
progress = exactProgress;
|
| 176 |
+
|
| 177 |
+
if (loaderPercent) {
|
| 178 |
+
loaderPercent.textContent = Math.floor(progress);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
if (allChars.length > 0) {
|
| 182 |
+
const totalChars = allChars.length;
|
| 183 |
+
// Calculate how many characters should be lit up based on progress
|
| 184 |
+
// We want all chars lit by 100%
|
| 185 |
+
const charsToLight = Math.floor((progress / 100) * totalChars);
|
| 186 |
+
|
| 187 |
+
allChars.forEach((charSpan, index) => {
|
| 188 |
+
if (index < charsToLight) {
|
| 189 |
+
charSpan.className = 'char-typed';
|
| 190 |
+
} else if (index === charsToLight && index < totalChars) {
|
| 191 |
+
charSpan.className = 'char-current';
|
| 192 |
+
} else {
|
| 193 |
+
charSpan.className = 'char-waiting';
|
| 194 |
+
}
|
| 195 |
+
});
|
| 196 |
+
} else if (loaderQuote) {
|
| 197 |
+
// Fallback if processing failed
|
| 198 |
+
loaderQuote.style.opacity = progress / 100;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// Only finish when minimum time has elapsed AND backend is ready
|
| 202 |
+
if (elapsed >= minimumDuration && progress >= 100 && window.isBackendReady === true) {
|
| 203 |
+
// Ensure we show 100%
|
| 204 |
+
if (loaderPercent) {
|
| 205 |
+
loaderPercent.textContent = '100';
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
// Reached 100% - Exit loader
|
| 209 |
+
setTimeout(() => {
|
| 210 |
+
console.log("Loader finished.");
|
| 211 |
+
clearInterval(statusInterval);
|
| 212 |
+
clearInterval(timeInterval);
|
| 213 |
+
|
| 214 |
+
loaderWrapper.classList.add('loaded'); // CSS transform
|
| 215 |
+
|
| 216 |
+
// Remove from DOM after animation
|
| 217 |
+
setTimeout(() => {
|
| 218 |
+
loaderWrapper.style.display = 'none';
|
| 219 |
+
}, 800); // Wait for CSS transition
|
| 220 |
+
}, 500); // Brief pause at 100%
|
| 221 |
+
} else {
|
| 222 |
+
requestAnimationFrame(animateLoader);
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
// Start countdown
|
| 227 |
+
animateLoader();
|
| 228 |
+
};
|
| 229 |
+
|
| 230 |
+
// Robust initialization
|
| 231 |
+
if (document.readyState === 'loading') {
|
| 232 |
+
document.addEventListener('DOMContentLoaded', initLoader);
|
| 233 |
+
} else {
|
| 234 |
+
// DOM already ready, run immediately
|
| 235 |
+
initLoader();
|
| 236 |
+
}
|
frontend/logo.ico
ADDED
|
|
Git LFS Details
|
frontend/logo.svg
ADDED
|
|
frontend/manifest.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "DeepGuard - AI Deepfake Detection",
|
| 3 |
+
"short_name": "DeepGuard",
|
| 4 |
+
"description": "Advanced AI-powered deepfake detection system using cutting-edge machine learning to identify manipulated media with unprecedented accuracy.",
|
| 5 |
+
"start_url": "/index.html",
|
| 6 |
+
"scope": "/",
|
| 7 |
+
"display": "standalone",
|
| 8 |
+
"background_color": "#000000",
|
| 9 |
+
"theme_color": "#E3F514",
|
| 10 |
+
"orientation": "portrait-primary",
|
| 11 |
+
"icons": [
|
| 12 |
+
{
|
| 13 |
+
"src": "icon-192.png",
|
| 14 |
+
"sizes": "192x192",
|
| 15 |
+
"type": "image/png"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"src": "icon-512.png",
|
| 19 |
+
"sizes": "512x512",
|
| 20 |
+
"type": "image/png"
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"src": "logo.ico",
|
| 24 |
+
"sizes": "16x16 32x32 48x48 64x64",
|
| 25 |
+
"type": "image/x-icon"
|
| 26 |
+
}
|
| 27 |
+
],
|
| 28 |
+
"screenshots": [
|
| 29 |
+
{
|
| 30 |
+
"src": "/assets/screenshot-desktop.png",
|
| 31 |
+
"sizes": "1280x720",
|
| 32 |
+
"type": "image/png",
|
| 33 |
+
"form_factor": "wide",
|
| 34 |
+
"label": "DeepGuard Desktop View"
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"src": "/assets/screenshot-mobile.png",
|
| 38 |
+
"sizes": "750x1334",
|
| 39 |
+
"type": "image/png",
|
| 40 |
+
"form_factor": "narrow",
|
| 41 |
+
"label": "DeepGuard Mobile View"
|
| 42 |
+
}
|
| 43 |
+
],
|
| 44 |
+
"shortcuts": [
|
| 45 |
+
{
|
| 46 |
+
"name": "Analyze Media",
|
| 47 |
+
"short_name": "Analyze",
|
| 48 |
+
"description": "Start analyzing media for deepfakes",
|
| 49 |
+
"url": "/analysis.html",
|
| 50 |
+
"icons": [
|
| 51 |
+
{
|
| 52 |
+
"src": "/icon-192.png",
|
| 53 |
+
"sizes": "192x192",
|
| 54 |
+
"type": "image/png"
|
| 55 |
+
}
|
| 56 |
+
]
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"name": "View History",
|
| 60 |
+
"short_name": "History",
|
| 61 |
+
"description": "View scan history",
|
| 62 |
+
"url": "/history.html",
|
| 63 |
+
"icons": [
|
| 64 |
+
{
|
| 65 |
+
"src": "logo.ico",
|
| 66 |
+
"sizes": "192x192",
|
| 67 |
+
"type": "image/png"
|
| 68 |
+
}
|
| 69 |
+
]
|
| 70 |
+
}
|
| 71 |
+
],
|
| 72 |
+
"categories": [
|
| 73 |
+
"productivity",
|
| 74 |
+
"utilities",
|
| 75 |
+
"security"
|
| 76 |
+
],
|
| 77 |
+
"iarc_rating_id": "e84b072d-71b3-4d3e-86ae-31a8ce4e53b7",
|
| 78 |
+
"prefer_related_applications": false,
|
| 79 |
+
"related_applications": [],
|
| 80 |
+
"share_target": {
|
| 81 |
+
"action": "/analysis.html",
|
| 82 |
+
"method": "GET",
|
| 83 |
+
"enctype": "application/x-www-form-urlencoded",
|
| 84 |
+
"params": {
|
| 85 |
+
"title": "title",
|
| 86 |
+
"text": "text",
|
| 87 |
+
"url": "url"
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
}
|
frontend/mobile.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Mobile Navigation & Utilities
|
| 3 |
+
* Handles hamburger menu, touch events, and mobile-specific optimizations
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
(function () {
|
| 7 |
+
'use strict';
|
| 8 |
+
|
| 9 |
+
// ==================== HAMBURGER MENU ====================
|
| 10 |
+
const hamburger = document.getElementById('hamburger');
|
| 11 |
+
const navMenuWrapper = document.querySelector('.nav-menu-wrapper');
|
| 12 |
+
const body = document.body;
|
| 13 |
+
|
| 14 |
+
if (hamburger && navMenuWrapper) {
|
| 15 |
+
// Toggle menu
|
| 16 |
+
hamburger.addEventListener('click', function () {
|
| 17 |
+
this.classList.toggle('active');
|
| 18 |
+
navMenuWrapper.classList.toggle('active');
|
| 19 |
+
body.classList.toggle('menu-open');
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
// Close menu when clicking on a nav link
|
| 23 |
+
const navLinks = navMenuWrapper.querySelectorAll('.nav-menu a, .btn-primary');
|
| 24 |
+
navLinks.forEach(link => {
|
| 25 |
+
link.addEventListener('click', function () {
|
| 26 |
+
hamburger.classList.remove('active');
|
| 27 |
+
navMenuWrapper.classList.remove('active');
|
| 28 |
+
body.classList.remove('menu-open');
|
| 29 |
+
});
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
// Close menu when clicking outside
|
| 33 |
+
document.addEventListener('click', function (event) {
|
| 34 |
+
const isClickInsideNav = navMenuWrapper.contains(event.target);
|
| 35 |
+
const isClickOnHamburger = hamburger.contains(event.target);
|
| 36 |
+
|
| 37 |
+
if (!isClickInsideNav && !isClickOnHamburger && navMenuWrapper.classList.contains('active')) {
|
| 38 |
+
hamburger.classList.remove('active');
|
| 39 |
+
navMenuWrapper.classList.remove('active');
|
| 40 |
+
body.classList.remove('menu-open');
|
| 41 |
+
}
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
// Close menu on ESC key
|
| 45 |
+
document.addEventListener('keydown', function (event) {
|
| 46 |
+
if (event.key === 'Escape' && navMenuWrapper.classList.contains('active')) {
|
| 47 |
+
hamburger.classList.remove('active');
|
| 48 |
+
navMenuWrapper.classList.remove('active');
|
| 49 |
+
body.classList.remove('menu-open');
|
| 50 |
+
}
|
| 51 |
+
});
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// ==================== VIEWPORT HEIGHT FIX (iOS) ====================
|
| 55 |
+
// Fix for 100vh on mobile browsers (address bar issue)
|
| 56 |
+
function setViewportHeight() {
|
| 57 |
+
const vh = window.innerHeight * 0.01;
|
| 58 |
+
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
setViewportHeight();
|
| 62 |
+
window.addEventListener('resize', setViewportHeight);
|
| 63 |
+
window.addEventListener('orientationchange', setViewportHeight);
|
| 64 |
+
|
| 65 |
+
// ==================== TOUCH IMPROVEMENTS ====================
|
| 66 |
+
// Add touch-active class for better touch feedback
|
| 67 |
+
document.querySelectorAll('button, a, .tech-card, .showcase-item, .history-card').forEach(element => {
|
| 68 |
+
element.addEventListener('touchstart', function () {
|
| 69 |
+
this.classList.add('touch-active');
|
| 70 |
+
}, { passive: true });
|
| 71 |
+
|
| 72 |
+
element.addEventListener('touchend', function () {
|
| 73 |
+
this.classList.remove('touch-active');
|
| 74 |
+
}, { passive: true });
|
| 75 |
+
|
| 76 |
+
element.addEventListener('touchcancel', function () {
|
| 77 |
+
this.classList.remove('touch-active');
|
| 78 |
+
}, { passive: true });
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
// ==================== PREVENT ZOOM ON INPUT FOCUS ====================
|
| 82 |
+
// Already handled in CSS with font-size: 16px, but adding for completeness
|
| 83 |
+
const inputs = document.querySelectorAll('input, textarea, select');
|
| 84 |
+
inputs.forEach(input => {
|
| 85 |
+
input.addEventListener('focus', function () {
|
| 86 |
+
const viewport = document.querySelector('meta[name=viewport]');
|
| 87 |
+
if (viewport) {
|
| 88 |
+
viewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0';
|
| 89 |
+
}
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
input.addEventListener('blur', function () {
|
| 93 |
+
const viewport = document.querySelector('meta[name=viewport]');
|
| 94 |
+
if (viewport) {
|
| 95 |
+
viewport.content = 'width=device-width, initial-scale=1.0';
|
| 96 |
+
}
|
| 97 |
+
});
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
+
// ==================== MOBILE DETECTION ====================
|
| 101 |
+
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
| 102 |
+
const isTablet = /(iPad|tablet|(android(?!.*mobile))|(windows(?!.*phone)(.*touch))|kindle|playbook|silk|(puffin(?!.*(IP|AP|WP))))/.test(navigator.userAgent.toLowerCase());
|
| 103 |
+
|
| 104 |
+
if (isMobile) {
|
| 105 |
+
document.body.classList.add('is-mobile');
|
| 106 |
+
}
|
| 107 |
+
if (isTablet) {
|
| 108 |
+
document.body.classList.add('is-tablet');
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// ==================== SMOOTH SCROLL POLYFILL ====================
|
| 112 |
+
// For browsers that don't support smooth scrolling
|
| 113 |
+
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
| 114 |
+
anchor.addEventListener('click', function (e) {
|
| 115 |
+
const target = document.querySelector(this.getAttribute('href'));
|
| 116 |
+
if (target) {
|
| 117 |
+
e.preventDefault();
|
| 118 |
+
target.scrollIntoView({
|
| 119 |
+
behavior: 'smooth',
|
| 120 |
+
block: 'start'
|
| 121 |
+
});
|
| 122 |
+
}
|
| 123 |
+
});
|
| 124 |
+
});
|
| 125 |
+
|
| 126 |
+
// ==================== DEBOUNCED RESIZE HANDLER ====================
|
| 127 |
+
let resizeTimer;
|
| 128 |
+
window.addEventListener('resize', function () {
|
| 129 |
+
clearTimeout(resizeTimer);
|
| 130 |
+
resizeTimer = setTimeout(function () {
|
| 131 |
+
// Trigger custom event that other scripts can listen to
|
| 132 |
+
window.dispatchEvent(new CustomEvent('debouncedResize'));
|
| 133 |
+
}, 250);
|
| 134 |
+
});
|
| 135 |
+
|
| 136 |
+
// ==================== LAZY LOAD OPTIMIZATION ====================
|
| 137 |
+
// Only load images when they're about to enter the viewport
|
| 138 |
+
if ('IntersectionObserver' in window) {
|
| 139 |
+
const imageObserver = new IntersectionObserver((entries, observer) => {
|
| 140 |
+
entries.forEach(entry => {
|
| 141 |
+
if (entry.isIntersecting) {
|
| 142 |
+
const img = entry.target;
|
| 143 |
+
if (img.dataset.src) {
|
| 144 |
+
img.src = img.dataset.src;
|
| 145 |
+
img.removeAttribute('data-src');
|
| 146 |
+
observer.unobserve(img);
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
});
|
| 150 |
+
}, {
|
| 151 |
+
rootMargin: '50px'
|
| 152 |
+
});
|
| 153 |
+
|
| 154 |
+
document.querySelectorAll('img[data-src]').forEach(img => {
|
| 155 |
+
imageObserver.observe(img);
|
| 156 |
+
});
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
// ==================== PREVENT OVERSCROLL (iOS) ====================
|
| 160 |
+
// Prevent rubber-band scrolling on iOS
|
| 161 |
+
let scrollStartY = 0;
|
| 162 |
+
|
| 163 |
+
document.addEventListener('touchstart', function (e) {
|
| 164 |
+
scrollStartY = e.touches[0].pageY;
|
| 165 |
+
}, { passive: true });
|
| 166 |
+
|
| 167 |
+
document.addEventListener('touchmove', function (e) {
|
| 168 |
+
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
|
| 169 |
+
const scrollHeight = document.documentElement.scrollHeight;
|
| 170 |
+
const clientHeight = document.documentElement.clientHeight;
|
| 171 |
+
const scrollY = e.touches[0].pageY;
|
| 172 |
+
|
| 173 |
+
// Prevent overscroll at top
|
| 174 |
+
if (scrollTop === 0 && scrollY > scrollStartY) {
|
| 175 |
+
e.preventDefault();
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
// Prevent overscroll at bottom
|
| 179 |
+
if (scrollTop + clientHeight >= scrollHeight && scrollY < scrollStartY) {
|
| 180 |
+
e.preventDefault();
|
| 181 |
+
}
|
| 182 |
+
}, { passive: false });
|
| 183 |
+
|
| 184 |
+
// ==================== PERFORMANCE OPTIMIZATION ====================
|
| 185 |
+
// Reduce animations on low-end devices
|
| 186 |
+
if (navigator.hardwareConcurrency && navigator.hardwareConcurrency < 4) {
|
| 187 |
+
document.body.classList.add('reduce-motion');
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// Detect slow connection
|
| 191 |
+
if ('connection' in navigator) {
|
| 192 |
+
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
| 193 |
+
if (connection && (connection.effectiveType === '2g' || connection.effectiveType === 'slow-2g')) {
|
| 194 |
+
document.body.classList.add('slow-connection');
|
| 195 |
+
// Disable heavy animations
|
| 196 |
+
document.querySelectorAll('.floating-3d-object').forEach(el => {
|
| 197 |
+
el.style.display = 'none';
|
| 198 |
+
});
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// ==================== HORIZONTAL SCROLL INDICATOR ====================
|
| 203 |
+
// Add scroll indicator for tables on mobile
|
| 204 |
+
const scrollableElements = document.querySelectorAll('.history-table-container, .pipeline');
|
| 205 |
+
scrollableElements.forEach(element => {
|
| 206 |
+
if (element.scrollWidth > element.clientWidth) {
|
| 207 |
+
element.classList.add('has-horizontal-scroll');
|
| 208 |
+
|
| 209 |
+
// Remove indicator after first scroll
|
| 210 |
+
element.addEventListener('scroll', function () {
|
| 211 |
+
this.classList.remove('has-horizontal-scroll');
|
| 212 |
+
}, { once: true });
|
| 213 |
+
}
|
| 214 |
+
});
|
| 215 |
+
|
| 216 |
+
// ==================== STATUS BAR COLOR (PWA) ====================
|
| 217 |
+
// Set theme color for mobile browsers
|
| 218 |
+
const metaThemeColor = document.querySelector('meta[name=theme-color]');
|
| 219 |
+
if (!metaThemeColor) {
|
| 220 |
+
const meta = document.createElement('meta');
|
| 221 |
+
meta.name = 'theme-color';
|
| 222 |
+
meta.content = '#000000';
|
| 223 |
+
document.head.appendChild(meta);
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
console.log('🚀 Mobile optimizations loaded');
|
| 227 |
+
})();
|
frontend/motion.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* DeepGuard Motion Design System
|
| 3 |
+
* Implements: Lenis Smooth Scroll, Magnetic Buttons, Spotlight Cards, Text Reveals
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 7 |
+
const lenis = initSmoothScroll();
|
| 8 |
+
initMagneticButtons();
|
| 9 |
+
initSpotlightCards();
|
| 10 |
+
initTextReveals();
|
| 11 |
+
if (lenis) {
|
| 12 |
+
initParallax(lenis);
|
| 13 |
+
}
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
/* ==================== 1. SMOOTH SCROLL (LENIS) ==================== */
|
| 17 |
+
function initSmoothScroll() {
|
| 18 |
+
// Check if Lenis is loaded
|
| 19 |
+
if (typeof Lenis === 'undefined') {
|
| 20 |
+
console.warn('Lenis not loaded. Skipping smooth scroll.');
|
| 21 |
+
return null;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const lenis = new Lenis({
|
| 25 |
+
duration: 1.2,
|
| 26 |
+
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
| 27 |
+
direction: 'vertical',
|
| 28 |
+
gestureDirection: 'vertical',
|
| 29 |
+
smooth: true,
|
| 30 |
+
mouseMultiplier: 1,
|
| 31 |
+
smoothTouch: false,
|
| 32 |
+
touchMultiplier: 2,
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
function raf(time) {
|
| 36 |
+
lenis.raf(time);
|
| 37 |
+
requestAnimationFrame(raf);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
requestAnimationFrame(raf);
|
| 41 |
+
|
| 42 |
+
return lenis;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/* ==================== 2. MAGNETIC BUTTONS ==================== */
|
| 46 |
+
function initMagneticButtons() {
|
| 47 |
+
const buttons = document.querySelectorAll('.btn-primary, .btn-hero-primary');
|
| 48 |
+
|
| 49 |
+
buttons.forEach(btn => {
|
| 50 |
+
btn.addEventListener('mousemove', (e) => {
|
| 51 |
+
const rect = btn.getBoundingClientRect();
|
| 52 |
+
const x = e.clientX - rect.left;
|
| 53 |
+
const y = e.clientY - rect.top;
|
| 54 |
+
|
| 55 |
+
// Calculate distance from center
|
| 56 |
+
const centerX = rect.width / 2;
|
| 57 |
+
const centerY = rect.height / 2;
|
| 58 |
+
|
| 59 |
+
const deltaX = (x - centerX) * 0.3; // Strength of pull
|
| 60 |
+
const deltaY = (y - centerY) * 0.3;
|
| 61 |
+
|
| 62 |
+
btn.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
btn.addEventListener('mouseleave', () => {
|
| 66 |
+
btn.style.transform = 'translate(0px, 0px)';
|
| 67 |
+
});
|
| 68 |
+
});
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* ==================== 3. SPOTLIGHT CARDS ==================== */
|
| 72 |
+
function initSpotlightCards() {
|
| 73 |
+
const cards = document.querySelectorAll('.feature-card, .showcase-item, .tech-card');
|
| 74 |
+
|
| 75 |
+
cards.forEach(card => {
|
| 76 |
+
card.addEventListener('mousemove', (e) => {
|
| 77 |
+
const rect = card.getBoundingClientRect();
|
| 78 |
+
const x = e.clientX - rect.left;
|
| 79 |
+
const y = e.clientY - rect.top;
|
| 80 |
+
|
| 81 |
+
card.style.setProperty('--mouse-x', `${x}px`);
|
| 82 |
+
card.style.setProperty('--mouse-y', `${y}px`);
|
| 83 |
+
});
|
| 84 |
+
});
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/* ==================== 4. TEXT REVEALS ==================== */
|
| 88 |
+
function initTextReveals() {
|
| 89 |
+
// Targets: Hero title, Section titles
|
| 90 |
+
const targets = document.querySelectorAll('.hero-title, .section-title');
|
| 91 |
+
|
| 92 |
+
const observer = new IntersectionObserver((entries) => {
|
| 93 |
+
entries.forEach(entry => {
|
| 94 |
+
if (entry.isIntersecting) {
|
| 95 |
+
entry.target.classList.add('in-view');
|
| 96 |
+
observer.unobserve(entry.target); // Only animate once
|
| 97 |
+
}
|
| 98 |
+
});
|
| 99 |
+
}, {
|
| 100 |
+
threshold: 0.2
|
| 101 |
+
});
|
| 102 |
+
|
| 103 |
+
targets.forEach(target => {
|
| 104 |
+
observer.observe(target);
|
| 105 |
+
});
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/* ==================== 5. PARALLAX EFFECTS ==================== */
|
| 109 |
+
function initParallax(lenis) {
|
| 110 |
+
const parallaxItems = document.querySelectorAll('[data-speed]');
|
| 111 |
+
|
| 112 |
+
if (parallaxItems.length === 0) return;
|
| 113 |
+
|
| 114 |
+
lenis.on('scroll', ({ scroll }) => {
|
| 115 |
+
parallaxItems.forEach(item => {
|
| 116 |
+
const speed = parseFloat(item.dataset.speed) || 0;
|
| 117 |
+
// Apply standard translation
|
| 118 |
+
// Note: This overrides other transforms, so use on dedicated wrappers or elements without other transforms
|
| 119 |
+
item.style.transform = `translateY(${scroll * speed}px)`;
|
| 120 |
+
});
|
| 121 |
+
});
|
| 122 |
+
}
|
frontend/offline.html
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
| 7 |
+
<title>Offline - DeepGuard</title>
|
| 8 |
+
<link rel="stylesheet" href="style.css">
|
| 9 |
+
<link rel="stylesheet" href="responsive-additions.css">
|
| 10 |
+
<style>
|
| 11 |
+
.offline-container {
|
| 12 |
+
min-height: 100vh;
|
| 13 |
+
display: flex;
|
| 14 |
+
flex-direction: column;
|
| 15 |
+
align-items: center;
|
| 16 |
+
justify-content: center;
|
| 17 |
+
text-align: center;
|
| 18 |
+
padding: 40px 20px;
|
| 19 |
+
background: var(--primary-bg);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.offline-icon {
|
| 23 |
+
font-size: 120px;
|
| 24 |
+
margin-bottom: 30px;
|
| 25 |
+
opacity: 0.3;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.offline-title {
|
| 29 |
+
font-size: 48px;
|
| 30 |
+
font-weight: 800;
|
| 31 |
+
color: var(--accent-yellow);
|
| 32 |
+
margin-bottom: 20px;
|
| 33 |
+
font-family: var(--font-display);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.offline-message {
|
| 37 |
+
font-size: 20px;
|
| 38 |
+
color: var(--text-secondary);
|
| 39 |
+
max-width: 500px;
|
| 40 |
+
margin: 0 auto 40px;
|
| 41 |
+
line-height: 1.6;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.offline-actions {
|
| 45 |
+
display: flex;
|
| 46 |
+
gap: 16px;
|
| 47 |
+
flex-wrap: wrap;
|
| 48 |
+
justify-content: center;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.btn-retry {
|
| 52 |
+
padding: 16px 32px;
|
| 53 |
+
background: var(--accent-yellow);
|
| 54 |
+
color: #000;
|
| 55 |
+
border: none;
|
| 56 |
+
border-radius: 12px;
|
| 57 |
+
font-size: 16px;
|
| 58 |
+
font-weight: 600;
|
| 59 |
+
cursor: pointer;
|
| 60 |
+
transition: all 0.3s ease;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.btn-retry:hover {
|
| 64 |
+
transform: translateY(-2px);
|
| 65 |
+
box-shadow: 0 10px 30px rgba(227, 245, 20, 0.3);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.btn-home {
|
| 69 |
+
padding: 16px 32px;
|
| 70 |
+
background: transparent;
|
| 71 |
+
color: var(--accent-yellow);
|
| 72 |
+
border: 2px solid var(--accent-yellow);
|
| 73 |
+
border-radius: 12px;
|
| 74 |
+
font-size: 16px;
|
| 75 |
+
font-weight: 600;
|
| 76 |
+
cursor: pointer;
|
| 77 |
+
transition: all 0.3s ease;
|
| 78 |
+
text-decoration: none;
|
| 79 |
+
display: inline-block;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.btn-home:hover {
|
| 83 |
+
background: rgba(227, 245, 20, 0.1);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.offline-tips {
|
| 87 |
+
margin-top: 60px;
|
| 88 |
+
padding: 30px;
|
| 89 |
+
background: rgba(255, 255, 255, 0.03);
|
| 90 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 91 |
+
border-radius: 16px;
|
| 92 |
+
max-width: 600px;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.offline-tips h3 {
|
| 96 |
+
color: var(--accent-yellow);
|
| 97 |
+
margin-bottom: 15px;
|
| 98 |
+
font-size: 18px;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.offline-tips ul {
|
| 102 |
+
list-style: none;
|
| 103 |
+
padding: 0;
|
| 104 |
+
text-align: left;
|
| 105 |
+
color: var(--text-secondary);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.offline-tips li {
|
| 109 |
+
padding: 8px 0;
|
| 110 |
+
padding-left: 30px;
|
| 111 |
+
position: relative;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.offline-tips li::before {
|
| 115 |
+
content: '•';
|
| 116 |
+
color: var(--accent-yellow);
|
| 117 |
+
position: absolute;
|
| 118 |
+
left: 0;
|
| 119 |
+
font-size: 24px;
|
| 120 |
+
}
|
| 121 |
+
</style>
|
| 122 |
+
</head>
|
| 123 |
+
|
| 124 |
+
<body>
|
| 125 |
+
<div class="offline-container">
|
| 126 |
+
<div class="offline-icon">📡</div>
|
| 127 |
+
<h1 class="offline-title">You're Offline</h1>
|
| 128 |
+
<p class="offline-message">
|
| 129 |
+
It looks like you've lost your internet connection. Some features may not be available until you're back
|
| 130 |
+
online.
|
| 131 |
+
</p>
|
| 132 |
+
|
| 133 |
+
<div class="offline-actions">
|
| 134 |
+
<button class="btn-retry" onclick="location.reload()">
|
| 135 |
+
🔄 Try Again
|
| 136 |
+
</button>
|
| 137 |
+
<a href="/index.html" class="btn-home">
|
| 138 |
+
🏠 Go Home
|
| 139 |
+
</a>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
<div class="offline-tips">
|
| 143 |
+
<h3>While You're Offline:</h3>
|
| 144 |
+
<ul>
|
| 145 |
+
<li>You can still browse previously loaded pages</li>
|
| 146 |
+
<li>Cached content remains available</li>
|
| 147 |
+
<li>Analysis history is accessible</li>
|
| 148 |
+
<li>New analyses require an internet connection</li>
|
| 149 |
+
</ul>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
<script>
|
| 154 |
+
// Auto-retry when online
|
| 155 |
+
window.addEventListener('online', () => {
|
| 156 |
+
console.log('Connection restored');
|
| 157 |
+
setTimeout(() => {
|
| 158 |
+
location.reload();
|
| 159 |
+
}, 1000);
|
| 160 |
+
});
|
| 161 |
+
|
| 162 |
+
// Check connection status
|
| 163 |
+
if (navigator.onLine) {
|
| 164 |
+
console.log('Online - attempting to reload');
|
| 165 |
+
location.reload();
|
| 166 |
+
}
|
| 167 |
+
</script>
|
| 168 |
+
</body>
|
| 169 |
+
|
| 170 |
+
</html>
|
frontend/orbit.css
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* 3D Orbit - Stabilized & High Fidelity */
|
| 2 |
+
.orbit-section {
|
| 3 |
+
padding: 100px 0;
|
| 4 |
+
position: relative;
|
| 5 |
+
overflow: hidden;
|
| 6 |
+
background: var(--primary-bg);
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
.orbit-container {
|
| 10 |
+
position: relative;
|
| 11 |
+
height: 750px;
|
| 12 |
+
width: 100%;
|
| 13 |
+
display: flex;
|
| 14 |
+
align-items: center;
|
| 15 |
+
justify-content: center;
|
| 16 |
+
perspective: 1500px;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/* 2D Overlay Elements - High Priority */
|
| 20 |
+
.orbit-info-card {
|
| 21 |
+
position: absolute;
|
| 22 |
+
top: 50%;
|
| 23 |
+
left: 50%;
|
| 24 |
+
transform: translate(-50%, -50%) scale(0.9);
|
| 25 |
+
width: 440px;
|
| 26 |
+
padding: 40px;
|
| 27 |
+
background: rgba(5, 5, 5, 0.98);
|
| 28 |
+
backdrop-filter: blur(30px);
|
| 29 |
+
border: 2px solid var(--accent-yellow);
|
| 30 |
+
box-shadow: 0 0 100px rgba(0, 0, 0, 0.95);
|
| 31 |
+
border-radius: 35px;
|
| 32 |
+
text-align: center;
|
| 33 |
+
opacity: 0;
|
| 34 |
+
visibility: hidden;
|
| 35 |
+
transition: transform 0.6s cubic-bezier(0.19, 1, 0.22, 1),
|
| 36 |
+
opacity 0.4s ease,
|
| 37 |
+
visibility 0.4s;
|
| 38 |
+
z-index: 2000;
|
| 39 |
+
/* Absolute Top */
|
| 40 |
+
pointer-events: auto;
|
| 41 |
+
backface-visibility: hidden;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.orbit-info-card.active {
|
| 45 |
+
opacity: 1;
|
| 46 |
+
visibility: visible;
|
| 47 |
+
transform: translate(-50%, -50%) scale(1.0);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.orbit-info-title {
|
| 51 |
+
font-size: 26px;
|
| 52 |
+
font-weight: 800;
|
| 53 |
+
color: var(--accent-yellow);
|
| 54 |
+
margin-bottom: 12px;
|
| 55 |
+
text-shadow: 0 0 15px rgba(227, 245, 20, 0.3);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.orbit-info-desc {
|
| 59 |
+
color: var(--text-secondary);
|
| 60 |
+
line-height: 1.8;
|
| 61 |
+
font-size: 16px;
|
| 62 |
+
margin-bottom: 25px;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.orbit-tag {
|
| 66 |
+
font-size: 11px;
|
| 67 |
+
padding: 6px 14px;
|
| 68 |
+
background: rgba(227, 245, 20, 0.1);
|
| 69 |
+
border: 1px solid rgba(227, 245, 20, 0.3);
|
| 70 |
+
color: var(--accent-yellow);
|
| 71 |
+
border-radius: 25px;
|
| 72 |
+
font-weight: 700;
|
| 73 |
+
margin: 0 5px;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* 3D Stage Space */
|
| 77 |
+
.orbit-stage {
|
| 78 |
+
position: absolute;
|
| 79 |
+
width: 1000px;
|
| 80 |
+
height: 1000px;
|
| 81 |
+
transform-style: preserve-3d;
|
| 82 |
+
transform: rotateX(65deg);
|
| 83 |
+
/* The fixed 3D slant */
|
| 84 |
+
display: flex;
|
| 85 |
+
align-items: center;
|
| 86 |
+
justify-content: center;
|
| 87 |
+
pointer-events: none;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.orbit-hub {
|
| 91 |
+
position: absolute;
|
| 92 |
+
width: 300px;
|
| 93 |
+
height: 300px;
|
| 94 |
+
transform: rotateX(-65deg);
|
| 95 |
+
display: flex;
|
| 96 |
+
align-items: center;
|
| 97 |
+
justify-content: center;
|
| 98 |
+
z-index: 5;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.orbit-hub-inner {
|
| 102 |
+
width: 140px;
|
| 103 |
+
height: 140px;
|
| 104 |
+
background: #000;
|
| 105 |
+
border: 3px solid var(--accent-yellow);
|
| 106 |
+
border-radius: 50%;
|
| 107 |
+
display: flex;
|
| 108 |
+
align-items: center;
|
| 109 |
+
justify-content: center;
|
| 110 |
+
font-size: 55px;
|
| 111 |
+
box-shadow: 0 0 50px rgba(227, 245, 20, 0.3);
|
| 112 |
+
animation: hub-pulse-v3 5s infinite ease-in-out;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
@keyframes hub-pulse-v3 {
|
| 116 |
+
|
| 117 |
+
0%,
|
| 118 |
+
100% {
|
| 119 |
+
transform: scale(1);
|
| 120 |
+
box-shadow: 0 0 50px rgba(227, 245, 20, 0.3);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
50% {
|
| 124 |
+
transform: scale(1.1);
|
| 125 |
+
box-shadow: 0 0 80px rgba(227, 245, 20, 0.5);
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
/* Spinning Plane */
|
| 130 |
+
.orbit-ring {
|
| 131 |
+
position: absolute;
|
| 132 |
+
width: 750px;
|
| 133 |
+
height: 750px;
|
| 134 |
+
transform-style: preserve-3d;
|
| 135 |
+
animation: orbit-main-v4 45s linear infinite;
|
| 136 |
+
pointer-events: none;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/* Smooth Pause */
|
| 140 |
+
.orbit-ring.paused,
|
| 141 |
+
.orbit-ring.paused .node-content {
|
| 142 |
+
animation-play-state: paused;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
@keyframes orbit-main-v4 {
|
| 146 |
+
from {
|
| 147 |
+
transform: rotateZ(0deg);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
to {
|
| 151 |
+
transform: rotateZ(360deg);
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/* Nodes */
|
| 156 |
+
.orbit-node {
|
| 157 |
+
position: absolute;
|
| 158 |
+
width: 120px;
|
| 159 |
+
height: 120px;
|
| 160 |
+
background: #000;
|
| 161 |
+
border: 2px solid rgba(255, 255, 255, 0.15);
|
| 162 |
+
border-radius: 50%;
|
| 163 |
+
display: flex;
|
| 164 |
+
align-items: center;
|
| 165 |
+
justify-content: center;
|
| 166 |
+
cursor: pointer;
|
| 167 |
+
pointer-events: auto;
|
| 168 |
+
z-index: 100;
|
| 169 |
+
/* Use transitions carefully to avoid glitching during ring rotation */
|
| 170 |
+
transition: border-color 0.4s ease, box-shadow 0.4s ease, transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 171 |
+
backface-visibility: hidden;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.orbit-node:hover {
|
| 175 |
+
border-color: var(--accent-yellow);
|
| 176 |
+
box-shadow: 0 0 40px rgba(227, 245, 20, 0.5);
|
| 177 |
+
transform: scale(1.3);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.node-content {
|
| 181 |
+
width: 100%;
|
| 182 |
+
height: 100%;
|
| 183 |
+
display: flex;
|
| 184 |
+
align-items: center;
|
| 185 |
+
justify-content: center;
|
| 186 |
+
/* Billboard */
|
| 187 |
+
transform: rotateX(-65deg);
|
| 188 |
+
animation: counter-v4 45s linear infinite;
|
| 189 |
+
backface-visibility: hidden;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
@keyframes counter-v4 {
|
| 193 |
+
from {
|
| 194 |
+
transform: rotateX(-65deg) rotateZ(0deg);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
to {
|
| 198 |
+
transform: rotateX(-65deg) rotateZ(-360deg);
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.node-icon {
|
| 203 |
+
width: 70px;
|
| 204 |
+
height: 70px;
|
| 205 |
+
object-fit: contain;
|
| 206 |
+
/* Fix for white backgrounds in icons - Blend them out */
|
| 207 |
+
mix-blend-mode: screen;
|
| 208 |
+
filter: drop-shadow(0 0 10px rgba(227, 245, 20, 0.3));
|
| 209 |
+
pointer-events: none;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
/* Static Positions */
|
| 213 |
+
.node-1 {
|
| 214 |
+
top: 0;
|
| 215 |
+
left: 50%;
|
| 216 |
+
transform: translate(-50%, -50%);
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.node-2 {
|
| 220 |
+
top: 50%;
|
| 221 |
+
right: 0;
|
| 222 |
+
transform: translate(50%, -50%);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.node-3 {
|
| 226 |
+
bottom: 0;
|
| 227 |
+
left: 50%;
|
| 228 |
+
transform: translate(-50%, 50%);
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.node-4 {
|
| 232 |
+
top: 50%;
|
| 233 |
+
left: 0;
|
| 234 |
+
transform: translate(-50%, -50%);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
/* Mobile */
|
| 238 |
+
@media (max-width: 900px) {
|
| 239 |
+
.orbit-ring {
|
| 240 |
+
width: 500px;
|
| 241 |
+
height: 500px;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.node-1 {
|
| 245 |
+
transform: rotate(0deg) translate(250px);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.node-2 {
|
| 249 |
+
transform: rotate(90deg) translate(250px);
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.node-3 {
|
| 253 |
+
transform: rotate(180deg) translate(250px);
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.node-4 {
|
| 257 |
+
transform: rotate(270deg) translate(250px);
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.orbit-info-card {
|
| 261 |
+
width: 340px;
|
| 262 |
+
padding: 25px;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.orbit-node {
|
| 266 |
+
width: 90px;
|
| 267 |
+
height: 90px;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.node-icon {
|
| 271 |
+
width: 45px;
|
| 272 |
+
height: 45px;
|
| 273 |
+
}
|
| 274 |
+
}
|
frontend/orbit_interaction.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const featureData = {
|
| 2 |
+
fusion: {
|
| 3 |
+
title: "Hybrid Fusion Architecture",
|
| 4 |
+
desc: "Combines EfficientNetV2 for spatial details and Swin Transformer V2 for global context with frequency domain analysis.",
|
| 5 |
+
tags: ["CNN-ViT", "Multi-Modal", "Spatial-Temporal"]
|
| 6 |
+
},
|
| 7 |
+
realtime: {
|
| 8 |
+
title: "Real-Time Analysis",
|
| 9 |
+
desc: "Lightning-fast detection processing thousands of images per minute with GPU acceleration and optimized pipelines.",
|
| 10 |
+
tags: ["CUDA", "Batch Process", "Low Latency"]
|
| 11 |
+
},
|
| 12 |
+
accuracy: {
|
| 13 |
+
title: "97% Detection Accuracy",
|
| 14 |
+
desc: "Industry-leading precision in detecting AI-generated and manipulated media across various generation methods.",
|
| 15 |
+
tags: ["Verified", "Tested", "SOTA"]
|
| 16 |
+
},
|
| 17 |
+
analytics: {
|
| 18 |
+
title: "Advanced Analytics",
|
| 19 |
+
desc: "Comprehensive reports with confidence scores, heatmaps, and detailed forensic analysis for every scan.",
|
| 20 |
+
tags: ["Reports", "Forensics", "Heatmaps"]
|
| 21 |
+
}
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
function initRobustOrbit() {
|
| 25 |
+
const nodes = document.querySelectorAll('.orbit-node');
|
| 26 |
+
const ring = document.querySelector('.orbit-ring');
|
| 27 |
+
const card = document.getElementById('orbitInfoCard');
|
| 28 |
+
const title = document.getElementById('orbitTitle');
|
| 29 |
+
const desc = document.getElementById('orbitDesc');
|
| 30 |
+
const tagsContainer = document.getElementById('orbitTags');
|
| 31 |
+
const hub = document.querySelector('.orbit-hub-inner');
|
| 32 |
+
|
| 33 |
+
if (!nodes.length || !ring || !card) return;
|
| 34 |
+
|
| 35 |
+
let hoverTimeout;
|
| 36 |
+
let isHoveringNode = false;
|
| 37 |
+
let isHoveringCard = false;
|
| 38 |
+
|
| 39 |
+
const updateState = () => {
|
| 40 |
+
if (isHoveringNode || isHoveringCard) {
|
| 41 |
+
clearTimeout(hoverTimeout);
|
| 42 |
+
card.classList.add('active');
|
| 43 |
+
ring.classList.add('paused');
|
| 44 |
+
if (hub) {
|
| 45 |
+
hub.style.boxShadow = "0 0 70px var(--accent-yellow)";
|
| 46 |
+
hub.textContent = "🔍";
|
| 47 |
+
}
|
| 48 |
+
} else {
|
| 49 |
+
hoverTimeout = setTimeout(() => {
|
| 50 |
+
if (!isHoveringNode && !isHoveringCard) {
|
| 51 |
+
card.classList.remove('active');
|
| 52 |
+
ring.classList.remove('paused');
|
| 53 |
+
if (hub) {
|
| 54 |
+
hub.style.boxShadow = "";
|
| 55 |
+
hub.textContent = "🛡️";
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
}, 150);
|
| 59 |
+
}
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
nodes.forEach(node => {
|
| 63 |
+
node.addEventListener('mouseenter', () => {
|
| 64 |
+
isHoveringNode = true;
|
| 65 |
+
const id = node.getAttribute('data-id');
|
| 66 |
+
const data = featureData[id];
|
| 67 |
+
if (data) {
|
| 68 |
+
title.textContent = data.title;
|
| 69 |
+
desc.textContent = data.desc;
|
| 70 |
+
tagsContainer.innerHTML = data.tags.map(t => `<span class="orbit-tag">${t}</span>`).join('');
|
| 71 |
+
}
|
| 72 |
+
updateState();
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
node.addEventListener('mouseleave', () => {
|
| 76 |
+
isHoveringNode = false;
|
| 77 |
+
updateState();
|
| 78 |
+
});
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
// Keeping it frozen when mouse is over the card itself
|
| 82 |
+
card.addEventListener('mouseenter', () => {
|
| 83 |
+
isHoveringCard = true;
|
| 84 |
+
updateState();
|
| 85 |
+
});
|
| 86 |
+
|
| 87 |
+
card.addEventListener('mouseleave', () => {
|
| 88 |
+
isHoveringCard = false;
|
| 89 |
+
updateState();
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
// Stability: Force animation refresh
|
| 93 |
+
requestAnimationFrame(() => {
|
| 94 |
+
ring.style.animation = 'none';
|
| 95 |
+
void ring.offsetWidth;
|
| 96 |
+
ring.style.animation = 'orbit-main-v4 45s linear infinite';
|
| 97 |
+
|
| 98 |
+
document.querySelectorAll('.node-content').forEach(nc => {
|
| 99 |
+
nc.style.animation = 'none';
|
| 100 |
+
void nc.offsetWidth;
|
| 101 |
+
nc.style.animation = 'counter-v4 45s linear infinite';
|
| 102 |
+
});
|
| 103 |
+
});
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// Multi-method startup
|
| 107 |
+
if (document.readyState === 'loading') {
|
| 108 |
+
document.addEventListener('DOMContentLoaded', initRobustOrbit);
|
| 109 |
+
} else {
|
| 110 |
+
initRobustOrbit();
|
| 111 |
+
}
|
| 112 |
+
window.addEventListener('load', initRobustOrbit);
|
frontend/pwa.css
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ==================== PWA STYLES ==================== */
|
| 2 |
+
|
| 3 |
+
/* PWA Install Button */
|
| 4 |
+
.pwa-install-button {
|
| 5 |
+
position: fixed;
|
| 6 |
+
bottom: 30px;
|
| 7 |
+
right: 30px;
|
| 8 |
+
z-index: 1000;
|
| 9 |
+
display: flex;
|
| 10 |
+
align-items: center;
|
| 11 |
+
gap: 10px;
|
| 12 |
+
padding: 14px 24px;
|
| 13 |
+
background: var(--accent-yellow);
|
| 14 |
+
color: #000;
|
| 15 |
+
border: none;
|
| 16 |
+
border-radius: 50px;
|
| 17 |
+
font-size: 15px;
|
| 18 |
+
font-weight: 600;
|
| 19 |
+
cursor: pointer;
|
| 20 |
+
box-shadow: 0 10px 40px rgba(227, 245, 20, 0.3);
|
| 21 |
+
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 22 |
+
opacity: 0;
|
| 23 |
+
transform: translateY(20px) scale(0.9);
|
| 24 |
+
pointer-events: none;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.pwa-install-button.visible {
|
| 28 |
+
opacity: 1;
|
| 29 |
+
transform: translateY(0) scale(1);
|
| 30 |
+
pointer-events: all;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.pwa-install-button:hover {
|
| 34 |
+
transform: translateY(-2px) scale(1.05);
|
| 35 |
+
box-shadow: 0 15px 50px rgba(227, 245, 20, 0.4);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.pwa-install-button:active {
|
| 39 |
+
transform: translateY(0) scale(0.98);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.pwa-install-button svg {
|
| 43 |
+
width: 20px;
|
| 44 |
+
height: 20px;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/* PWA Update Notification */
|
| 48 |
+
.pwa-update-notification {
|
| 49 |
+
position: fixed;
|
| 50 |
+
top: 20px;
|
| 51 |
+
left: 50%;
|
| 52 |
+
transform: translateX(-50%) translateY(-120%);
|
| 53 |
+
z-index: 10000;
|
| 54 |
+
background: rgba(0, 0, 0, 0.95);
|
| 55 |
+
backdrop-filter: blur(20px);
|
| 56 |
+
border: 1px solid rgba(227, 245, 20, 0.3);
|
| 57 |
+
border-radius: 16px;
|
| 58 |
+
padding: 0;
|
| 59 |
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
| 60 |
+
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 61 |
+
max-width: 500px;
|
| 62 |
+
width: 90%;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.pwa-update-notification.visible {
|
| 66 |
+
transform: translateX(-50%) translateY(0);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.update-content {
|
| 70 |
+
display: flex;
|
| 71 |
+
align-items: center;
|
| 72 |
+
gap: 16px;
|
| 73 |
+
padding: 20px;
|
| 74 |
+
position: relative;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.update-icon {
|
| 78 |
+
font-size: 40px;
|
| 79 |
+
flex-shrink: 0;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.update-text {
|
| 83 |
+
flex: 1;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.update-text strong {
|
| 87 |
+
color: var(--accent-yellow);
|
| 88 |
+
font-size: 16px;
|
| 89 |
+
display: block;
|
| 90 |
+
margin-bottom: 4px;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.update-text p {
|
| 94 |
+
color: #888;
|
| 95 |
+
font-size: 13px;
|
| 96 |
+
margin: 0;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.update-btn {
|
| 100 |
+
padding: 10px 20px;
|
| 101 |
+
background: var(--accent-yellow);
|
| 102 |
+
color: #000;
|
| 103 |
+
border: none;
|
| 104 |
+
border-radius: 8px;
|
| 105 |
+
font-size: 14px;
|
| 106 |
+
font-weight: 600;
|
| 107 |
+
cursor: pointer;
|
| 108 |
+
transition: all 0.2s ease;
|
| 109 |
+
flex-shrink: 0;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.update-btn:hover {
|
| 113 |
+
transform: translateY(-2px);
|
| 114 |
+
box-shadow: 0 5px 15px rgba(227, 245, 20, 0.3);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.update-dismiss {
|
| 118 |
+
position: absolute;
|
| 119 |
+
top: 10px;
|
| 120 |
+
right: 10px;
|
| 121 |
+
background: transparent;
|
| 122 |
+
border: none;
|
| 123 |
+
color: #666;
|
| 124 |
+
font-size: 18px;
|
| 125 |
+
cursor: pointer;
|
| 126 |
+
width: 30px;
|
| 127 |
+
height: 30px;
|
| 128 |
+
display: flex;
|
| 129 |
+
align-items: center;
|
| 130 |
+
justify-content: center;
|
| 131 |
+
border-radius: 50%;
|
| 132 |
+
transition: all 0.2s ease;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.update-dismiss:hover {
|
| 136 |
+
background: rgba(255, 255, 255, 0.1);
|
| 137 |
+
color: #fff;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/* iOS Install Prompt */
|
| 141 |
+
.ios-install-prompt {
|
| 142 |
+
position: fixed;
|
| 143 |
+
bottom: 0;
|
| 144 |
+
left: 0;
|
| 145 |
+
right: 0;
|
| 146 |
+
z-index: 10000;
|
| 147 |
+
background: rgba(0, 0, 0, 0.98);
|
| 148 |
+
backdrop-filter: blur(20px);
|
| 149 |
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
| 150 |
+
padding: 30px 20px;
|
| 151 |
+
transform: translateY(100%);
|
| 152 |
+
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 153 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.ios-install-prompt.visible {
|
| 157 |
+
transform: translateY(0);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.ios-prompt-content {
|
| 161 |
+
max-width: 500px;
|
| 162 |
+
margin: 0 auto;
|
| 163 |
+
position: relative;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.ios-prompt-close {
|
| 167 |
+
position: absolute;
|
| 168 |
+
top: -10px;
|
| 169 |
+
right: 0;
|
| 170 |
+
background: transparent;
|
| 171 |
+
border: none;
|
| 172 |
+
color: #666;
|
| 173 |
+
font-size: 24px;
|
| 174 |
+
cursor: pointer;
|
| 175 |
+
width: 40px;
|
| 176 |
+
height: 40px;
|
| 177 |
+
display: flex;
|
| 178 |
+
align-items: center;
|
| 179 |
+
justify-content: center;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.ios-prompt-icon {
|
| 183 |
+
text-align: center;
|
| 184 |
+
margin-bottom: 15px;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.ios-prompt-content h3 {
|
| 188 |
+
color: var(--accent-yellow);
|
| 189 |
+
text-align: center;
|
| 190 |
+
font-size: 20px;
|
| 191 |
+
margin-bottom: 10px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.ios-prompt-content p {
|
| 195 |
+
color: #888;
|
| 196 |
+
text-align: center;
|
| 197 |
+
margin-bottom: 15px;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.ios-prompt-content ol {
|
| 201 |
+
color: #fff;
|
| 202 |
+
padding-left: 20px;
|
| 203 |
+
font-size: 14px;
|
| 204 |
+
line-height: 1.8;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.ios-prompt-content ol li {
|
| 208 |
+
margin-bottom: 8px;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.ios-prompt-content ol strong {
|
| 212 |
+
color: var(--accent-yellow);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
/* PWA Mode Adjustments */
|
| 216 |
+
body.pwa-mode {
|
| 217 |
+
/* Add any PWA-specific styles */
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
/* iOS PWA Status Bar Spacing */
|
| 221 |
+
body.ios-pwa {
|
| 222 |
+
padding-top: env(safe-area-inset-top);
|
| 223 |
+
padding-bottom: env(safe-area-inset-bottom);
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
body.ios-pwa .navbar {
|
| 227 |
+
padding-top: calc(env(safe-area-inset-top) + 20px);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
/* Hide install button on mobile when already in PWA mode */
|
| 231 |
+
body.pwa-mode .pwa-install-button {
|
| 232 |
+
display: none;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
/* Mobile-specific adjustments */
|
| 236 |
+
@media (max-width: 768px) {
|
| 237 |
+
.pwa-install-button {
|
| 238 |
+
bottom: 20px;
|
| 239 |
+
right: 20px;
|
| 240 |
+
padding: 12px 20px;
|
| 241 |
+
font-size: 14px;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.pwa-update-notification {
|
| 245 |
+
top: 10px;
|
| 246 |
+
width: 95%;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.update-content {
|
| 250 |
+
flex-wrap: wrap;
|
| 251 |
+
padding: 15px;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.update-btn {
|
| 255 |
+
width: 100%;
|
| 256 |
+
margin-top: 10px;
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
/* Slideup animation for simple toast */
|
| 261 |
+
@keyframes slideUp {
|
| 262 |
+
from {
|
| 263 |
+
opacity: 0;
|
| 264 |
+
transform: translate(-50%, 20px);
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
to {
|
| 268 |
+
opacity: 1;
|
| 269 |
+
transform: translate(-50%, 0);
|
| 270 |
+
}
|
| 271 |
+
}
|
frontend/pwa.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* PWA Installation and Service Worker Registration
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
(function () {
|
| 6 |
+
'use strict';
|
| 7 |
+
|
| 8 |
+
// ==================== SERVICE WORKER REGISTRATION ====================
|
| 9 |
+
if ('serviceWorker' in navigator) {
|
| 10 |
+
window.addEventListener('load', () => {
|
| 11 |
+
navigator.serviceWorker.register('/service-worker.js')
|
| 12 |
+
.then((registration) => {
|
| 13 |
+
console.log('✅ Service Worker registered:', registration.scope);
|
| 14 |
+
|
| 15 |
+
// Check for updates
|
| 16 |
+
registration.addEventListener('updatefound', () => {
|
| 17 |
+
const newWorker = registration.installing;
|
| 18 |
+
console.log('🔄 Service Worker update found');
|
| 19 |
+
|
| 20 |
+
newWorker.addEventListener('statechange', () => {
|
| 21 |
+
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
| 22 |
+
// New version available
|
| 23 |
+
showUpdateNotification();
|
| 24 |
+
}
|
| 25 |
+
});
|
| 26 |
+
});
|
| 27 |
+
})
|
| 28 |
+
.catch((error) => {
|
| 29 |
+
console.error('❌ Service Worker registration failed:', error);
|
| 30 |
+
});
|
| 31 |
+
});
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// ==================== PWA INSTALL PROMPT ====================
|
| 35 |
+
let deferredPrompt;
|
| 36 |
+
let installButton;
|
| 37 |
+
|
| 38 |
+
// Listen for install prompt event
|
| 39 |
+
window.addEventListener('beforeinstallprompt', (e) => {
|
| 40 |
+
console.log('💾 Install prompt triggered');
|
| 41 |
+
|
| 42 |
+
// Prevent Chrome 67 and earlier from automatically showing the prompt
|
| 43 |
+
e.preventDefault();
|
| 44 |
+
|
| 45 |
+
// Store the event for later use
|
| 46 |
+
deferredPrompt = e;
|
| 47 |
+
|
| 48 |
+
// Show install button
|
| 49 |
+
showInstallButton();
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
// Create and show install button
|
| 53 |
+
function showInstallButton() {
|
| 54 |
+
// Check if already installed
|
| 55 |
+
if (window.matchMedia('(display-mode: standalone)').matches) {
|
| 56 |
+
console.log('Already installed as PWA');
|
| 57 |
+
return;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Check if button already exists
|
| 61 |
+
if (document.getElementById('pwa-install-btn')) return;
|
| 62 |
+
|
| 63 |
+
// Create install button
|
| 64 |
+
installButton = document.createElement('button');
|
| 65 |
+
installButton.id = 'pwa-install-btn';
|
| 66 |
+
installButton.className = 'pwa-install-button';
|
| 67 |
+
installButton.innerHTML = `
|
| 68 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 69 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
| 70 |
+
<polyline points="7 10 12 15 17 10"></polyline>
|
| 71 |
+
<line x1="12" y1="15" x2="12" y2="3"></line>
|
| 72 |
+
</svg>
|
| 73 |
+
<span>Install App</span>
|
| 74 |
+
`;
|
| 75 |
+
|
| 76 |
+
installButton.addEventListener('click', handleInstallClick);
|
| 77 |
+
|
| 78 |
+
// Add to page
|
| 79 |
+
document.body.appendChild(installButton);
|
| 80 |
+
|
| 81 |
+
// Fade in animation
|
| 82 |
+
setTimeout(() => {
|
| 83 |
+
installButton.classList.add('visible');
|
| 84 |
+
}, 100);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// Handle install button click
|
| 88 |
+
async function handleInstallClick() {
|
| 89 |
+
if (!deferredPrompt) return;
|
| 90 |
+
|
| 91 |
+
// Show install prompt
|
| 92 |
+
deferredPrompt.prompt();
|
| 93 |
+
|
| 94 |
+
// Wait for user choice
|
| 95 |
+
const { outcome } = await deferredPrompt.userChoice;
|
| 96 |
+
|
| 97 |
+
console.log(`User response to install prompt: ${outcome}`);
|
| 98 |
+
|
| 99 |
+
if (outcome === 'accepted') {
|
| 100 |
+
console.log('✅ PWA installed');
|
| 101 |
+
hideInstallButton();
|
| 102 |
+
} else {
|
| 103 |
+
console.log('❌ PWA installation declined');
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// Clear the deferredPrompt
|
| 107 |
+
deferredPrompt = null;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// Hide install button
|
| 111 |
+
function hideInstallButton() {
|
| 112 |
+
if (installButton) {
|
| 113 |
+
installButton.classList.remove('visible');
|
| 114 |
+
setTimeout(() => {
|
| 115 |
+
if (installButton && installButton.parentNode) {
|
| 116 |
+
installButton.remove();
|
| 117 |
+
}
|
| 118 |
+
}, 300);
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// ==================== DETECT PWA MODE ====================
|
| 123 |
+
window.addEventListener('DOMContentLoaded', () => {
|
| 124 |
+
// Check if running as installed PWA
|
| 125 |
+
const isStandalone = window.matchMedia('(display-mode: standalone)').matches ||
|
| 126 |
+
window.navigator.standalone ||
|
| 127 |
+
document.referrer.includes('android-app://');
|
| 128 |
+
|
| 129 |
+
if (isStandalone) {
|
| 130 |
+
console.log('🚀 Running as PWA');
|
| 131 |
+
document.body.classList.add('pwa-mode');
|
| 132 |
+
|
| 133 |
+
// Add iOS status bar spacing
|
| 134 |
+
if (navigator.userAgent.match(/iPhone|iPad|iPod/)) {
|
| 135 |
+
document.body.classList.add('ios-pwa');
|
| 136 |
+
}
|
| 137 |
+
} else {
|
| 138 |
+
console.log('🌐 Running in browser');
|
| 139 |
+
}
|
| 140 |
+
});
|
| 141 |
+
|
| 142 |
+
// ==================== UPDATE NOTIFICATION ====================
|
| 143 |
+
function showUpdateNotification() {
|
| 144 |
+
// Check if notification already exists
|
| 145 |
+
if (document.getElementById('pwa-update-notification')) return;
|
| 146 |
+
|
| 147 |
+
const notification = document.createElement('div');
|
| 148 |
+
notification.id = 'pwa-update-notification';
|
| 149 |
+
notification.className = 'pwa-update-notification';
|
| 150 |
+
notification.innerHTML = `
|
| 151 |
+
<div class="update-content">
|
| 152 |
+
<div class="update-icon">🔄</div>
|
| 153 |
+
<div class="update-text">
|
| 154 |
+
<strong>New version available!</strong>
|
| 155 |
+
<p>Click to update and get the latest features</p>
|
| 156 |
+
</div>
|
| 157 |
+
<button class="update-btn" onclick="window.location.reload()">
|
| 158 |
+
Update Now
|
| 159 |
+
</button>
|
| 160 |
+
<button class="update-dismiss" onclick="this.parentElement.parentElement.remove()">
|
| 161 |
+
✕
|
| 162 |
+
</button>
|
| 163 |
+
</div>
|
| 164 |
+
`;
|
| 165 |
+
|
| 166 |
+
document.body.appendChild(notification);
|
| 167 |
+
|
| 168 |
+
setTimeout(() => {
|
| 169 |
+
notification.classList.add('visible');
|
| 170 |
+
}, 100);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// ==================== ONLINE/OFFLINE STATUS ====================
|
| 174 |
+
window.addEventListener('online', () => {
|
| 175 |
+
console.log('🌐 Connection restored');
|
| 176 |
+
showToast('Back online!', 'success');
|
| 177 |
+
});
|
| 178 |
+
|
| 179 |
+
window.addEventListener('offline', () => {
|
| 180 |
+
console.log('📡 Connection lost');
|
| 181 |
+
showToast('You are offline. Some features may be limited.', 'warning');
|
| 182 |
+
});
|
| 183 |
+
|
| 184 |
+
// Helper function for toast (if not already defined)
|
| 185 |
+
function showToast(message, type = 'info') {
|
| 186 |
+
// Use existing toast function if available
|
| 187 |
+
if (window.showToast) {
|
| 188 |
+
window.showToast(message, type);
|
| 189 |
+
return;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
// Simple fallback toast
|
| 193 |
+
const toast = document.createElement('div');
|
| 194 |
+
toast.className = `simple-toast toast-${type}`;
|
| 195 |
+
toast.textContent = message;
|
| 196 |
+
toast.style.cssText = `
|
| 197 |
+
position: fixed;
|
| 198 |
+
bottom: 20px;
|
| 199 |
+
left: 50%;
|
| 200 |
+
transform: translateX(-50%);
|
| 201 |
+
background: ${type === 'success' ? '#10b981' : type === 'warning' ? '#f59e0b' : '#3b82f6'};
|
| 202 |
+
color: white;
|
| 203 |
+
padding: 12px 24px;
|
| 204 |
+
border-radius: 8px;
|
| 205 |
+
font-size: 14px;
|
| 206 |
+
z-index: 10000;
|
| 207 |
+
animation: slideUp 0.3s ease;
|
| 208 |
+
`;
|
| 209 |
+
document.body.appendChild(toast);
|
| 210 |
+
|
| 211 |
+
setTimeout(() => {
|
| 212 |
+
toast.remove();
|
| 213 |
+
}, 3000);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
// ==================== iOS ADD TO HOME SCREEN PROMPT ====================
|
| 217 |
+
function showIOSInstallPrompt() {
|
| 218 |
+
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
| 219 |
+
const isInStandaloneMode = ('standalone' in window.navigator) && (window.navigator.standalone);
|
| 220 |
+
|
| 221 |
+
if (isIOS && !isInStandaloneMode) {
|
| 222 |
+
// Check if user has seen this before
|
| 223 |
+
if (localStorage.getItem('ios-install-prompt-dismissed')) {
|
| 224 |
+
return;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
const prompt = document.createElement('div');
|
| 228 |
+
prompt.className = 'ios-install-prompt';
|
| 229 |
+
prompt.innerHTML = `
|
| 230 |
+
<div class="ios-prompt-content">
|
| 231 |
+
<button class="ios-prompt-close" onclick="this.parentElement.parentElement.remove(); localStorage.setItem('ios-install-prompt-dismissed', 'true');">✕</button>
|
| 232 |
+
<div class="ios-prompt-icon">
|
| 233 |
+
<img src="/logo.ico" alt="DeepGuard" style="width: 60px; height: 60px; border-radius: 12px;">
|
| 234 |
+
</div>
|
| 235 |
+
<h3>Install DeepGuard</h3>
|
| 236 |
+
<p>Install this app on your iPhone:</p>
|
| 237 |
+
<ol>
|
| 238 |
+
<li>Tap the <strong>Share</strong> button <svg width="16" height="20" viewBox="0 0 16 20" fill="#0066cc"><path d="M8 0l8 8h-5v12H5V8H0z"/></svg></li>
|
| 239 |
+
<li>Select <strong>Add to Home Screen</strong></li>
|
| 240 |
+
</ol>
|
| 241 |
+
</div>
|
| 242 |
+
`;
|
| 243 |
+
document.body.appendChild(prompt);
|
| 244 |
+
|
| 245 |
+
setTimeout(() => {
|
| 246 |
+
prompt.classList.add('visible');
|
| 247 |
+
}, 2000);
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
// Show iOS prompt after short delay
|
| 252 |
+
setTimeout(showIOSInstallPrompt, 3000);
|
| 253 |
+
|
| 254 |
+
console.log('📱 PWA features initialized');
|
| 255 |
+
})();
|
frontend/realtime_analysis_icon.png
ADDED
|
|
Git LFS Details
|
frontend/responsive-additions.css
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ==================== COMPREHENSIVE RESPONSIVE FIXES ==================== */
|
| 2 |
+
/* This file contains ALL responsive fixes for the entire frontend */
|
| 3 |
+
/* Preserves exact visual design on desktop while ensuring perfect mobile adaptation */
|
| 4 |
+
|
| 5 |
+
/* ==================== GLOBAL OVERFLOW PREVENTION ==================== */
|
| 6 |
+
/* ==================== GLOBAL OVERFLOW PREVENTION ==================== */
|
| 7 |
+
html,
|
| 8 |
+
body {
|
| 9 |
+
overflow-x: hidden !important;
|
| 10 |
+
max-width: 100%;
|
| 11 |
+
/* Changed from 100vw to avoid scrollbar width issues */
|
| 12 |
+
width: 100%;
|
| 13 |
+
overscroll-behavior-y: none;
|
| 14 |
+
/* Prevent bounce on mobile if handled by Lenis */
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/* Ensure all major containers respect viewport */
|
| 18 |
+
.container,
|
| 19 |
+
.analysis-container,
|
| 20 |
+
.hero,
|
| 21 |
+
.section,
|
| 22 |
+
[class*="-container"],
|
| 23 |
+
[class*="-wrapper"] {
|
| 24 |
+
max-width: 100%;
|
| 25 |
+
box-sizing: border-box;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/* ==================== UNIVERSAL MEDIA CONSTRAINTS ==================== */
|
| 29 |
+
img,
|
| 30 |
+
video,
|
| 31 |
+
canvas,
|
| 32 |
+
iframe {
|
| 33 |
+
max-width: 100%;
|
| 34 |
+
height: auto;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* ==================== LOADER / ANALYZING SCREEN FIXES ==================== */
|
| 38 |
+
@media (max-height: 700px) {
|
| 39 |
+
|
| 40 |
+
/* Fix loader cropping on short screens */
|
| 41 |
+
#loader-wrapper {
|
| 42 |
+
padding: 20px;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
#loader-quote {
|
| 46 |
+
font-size: clamp(1rem, 4vh, 1.2rem) !important;
|
| 47 |
+
/* Fluid scaling */
|
| 48 |
+
margin-bottom: 1rem;
|
| 49 |
+
max-width: 95%;
|
| 50 |
+
line-height: 1.2;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.counter-wrapper {
|
| 54 |
+
font-size: clamp(3rem, 8vh, 5rem) !important;
|
| 55 |
+
/* Stable scaling */
|
| 56 |
+
bottom: 15px;
|
| 57 |
+
right: 15px;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.detection-status {
|
| 61 |
+
font-size: 1.2rem !important;
|
| 62 |
+
margin-bottom: 10px;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.loader-meta {
|
| 66 |
+
bottom: 15px;
|
| 67 |
+
font-size: 12px;
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
@media (max-width: 480px) {
|
| 72 |
+
#loader-quote {
|
| 73 |
+
font-size: 1rem !important;
|
| 74 |
+
padding: 10px;
|
| 75 |
+
letter-spacing: 0.03em;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.counter-wrapper {
|
| 79 |
+
font-size: clamp(4rem, 12vw, 6rem) !important;
|
| 80 |
+
bottom: 10px;
|
| 81 |
+
right: 10px;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.percent-symbol {
|
| 85 |
+
font-size: 0.5em;
|
| 86 |
+
/* Generic relative size */
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
/* ==================== ANALYSIS PAGE: PROCESSING OVERLAY ==================== */
|
| 91 |
+
.processing-overlay {
|
| 92 |
+
max-width: 100%;
|
| 93 |
+
max-height: 100%;
|
| 94 |
+
height: 100dvh;
|
| 95 |
+
/* Dynamic viewport height */
|
| 96 |
+
overflow: hidden;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
@media (max-width: 768px) {
|
| 100 |
+
.processing-overlay {
|
| 101 |
+
padding: 20px;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.processing-title {
|
| 105 |
+
font-size: clamp(16px, 4vw, 18px) !important;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.processing-status {
|
| 109 |
+
font-size: 13px !important;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.processing-time {
|
| 113 |
+
font-size: 12px !important;
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
@media (max-height: 600px) {
|
| 118 |
+
.processing-overlay {
|
| 119 |
+
padding: 10px;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.processing-title {
|
| 123 |
+
font-size: 16px !important;
|
| 124 |
+
margin-bottom: 10px;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.processing-spinner {
|
| 128 |
+
width: 40px !important;
|
| 129 |
+
height: 40px !important;
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/* ==================== PREVIEW AREA: IMAGE/VIDEO CONSTRAINTS ==================== */
|
| 134 |
+
.preview-area {
|
| 135 |
+
max-width: 100%;
|
| 136 |
+
/* Use dvh for mobile browser bars */
|
| 137 |
+
max-height: calc(100dvh - 250px);
|
| 138 |
+
overflow: hidden;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.preview-area img,
|
| 142 |
+
.preview-area video,
|
| 143 |
+
.preview-area canvas {
|
| 144 |
+
max-width: 100% !important;
|
| 145 |
+
max-height: calc(100dvh - 300px) !important;
|
| 146 |
+
object-fit: contain;
|
| 147 |
+
width: auto;
|
| 148 |
+
height: auto;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
@media (max-width: 768px) {
|
| 152 |
+
.preview-area {
|
| 153 |
+
max-height: 400px;
|
| 154 |
+
min-height: auto;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.preview-area img,
|
| 158 |
+
.preview-area video,
|
| 159 |
+
.preview-area canvas {
|
| 160 |
+
max-height: 350px !important;
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
@media (max-height: 700px) {
|
| 165 |
+
.preview-area {
|
| 166 |
+
max-height: 300px;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.preview-area img,
|
| 170 |
+
.preview-area video,
|
| 171 |
+
.preview-area canvas {
|
| 172 |
+
max-height: 250px !important;
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
/* ==================== HEATMAP OVERLAY CONSTRAINTS ==================== */
|
| 177 |
+
.heatmap-overlay,
|
| 178 |
+
.heatmap-container {
|
| 179 |
+
max-width: 100%;
|
| 180 |
+
max-height: calc(100vh - 250px);
|
| 181 |
+
overflow: hidden;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.heatmap-overlay img,
|
| 185 |
+
.heatmap-container img {
|
| 186 |
+
max-width: 100% !important;
|
| 187 |
+
max-height: calc(100vh - 300px) !important;
|
| 188 |
+
object-fit: contain;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
@media (max-width: 768px) {
|
| 192 |
+
|
| 193 |
+
.heatmap-overlay,
|
| 194 |
+
.heatmap-container {
|
| 195 |
+
max-height: 400px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.heatmap-overlay img,
|
| 199 |
+
.heatmap-container img {
|
| 200 |
+
max-height: 350px !important;
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
/* ==================== VIDEO RESULT PAGE ==================== */
|
| 205 |
+
.video-preview-container {
|
| 206 |
+
max-width: 100%;
|
| 207 |
+
max-height: calc(100vh - 200px);
|
| 208 |
+
overflow: hidden;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.video-preview-container video {
|
| 212 |
+
max-width: 100% !important;
|
| 213 |
+
max-height: calc(100vh - 250px) !important;
|
| 214 |
+
width: auto;
|
| 215 |
+
height: auto;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
@media (max-width: 768px) {
|
| 219 |
+
.video-preview-container {
|
| 220 |
+
max-height: 400px;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.video-preview-container video {
|
| 224 |
+
max-height: 350px !important;
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
/* ==================== BOTTOM ACTION PANELS / SHEETS ==================== */
|
| 229 |
+
.action-panel,
|
| 230 |
+
.bottom-sheet,
|
| 231 |
+
.queue-footer,
|
| 232 |
+
[class*="action"] {
|
| 233 |
+
position: relative;
|
| 234 |
+
bottom: auto;
|
| 235 |
+
max-width: 100%;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
@media (max-width: 768px) {
|
| 239 |
+
|
| 240 |
+
.action-panel,
|
| 241 |
+
.bottom-sheet {
|
| 242 |
+
position: sticky;
|
| 243 |
+
bottom: 0;
|
| 244 |
+
left: 0;
|
| 245 |
+
right: 0;
|
| 246 |
+
padding: 12px 16px;
|
| 247 |
+
max-height: 30vh;
|
| 248 |
+
overflow-y: auto;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.queue-footer {
|
| 252 |
+
flex-direction: column;
|
| 253 |
+
gap: 10px;
|
| 254 |
+
padding: 12px;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.queue-footer button {
|
| 258 |
+
width: 100% !important;
|
| 259 |
+
min-height: 48px;
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
/* ==================== FILE QUEUE MOBILE OPTIMIZATION ==================== */
|
| 264 |
+
@media (max-width: 768px) {
|
| 265 |
+
.file-queue-container {
|
| 266 |
+
max-height: calc(100vh - 400px);
|
| 267 |
+
overflow-y: auto;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.file-queue-item {
|
| 271 |
+
padding: 12px;
|
| 272 |
+
gap: 8px;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.queue-header {
|
| 276 |
+
flex-direction: column;
|
| 277 |
+
gap: 12px;
|
| 278 |
+
align-items: stretch;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.queue-title {
|
| 282 |
+
font-size: 16px;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.queue-actions {
|
| 286 |
+
display: flex;
|
| 287 |
+
gap: 8px;
|
| 288 |
+
width: 100%;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.queue-actions button {
|
| 292 |
+
flex: 1;
|
| 293 |
+
min-height: 44px;
|
| 294 |
+
font-size: 13px;
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
/* ==================== MODALS & OVERLAYS ==================== */
|
| 299 |
+
.modal-overlay,
|
| 300 |
+
.overlay {
|
| 301 |
+
max-width: 100vw;
|
| 302 |
+
max-height: 100vh;
|
| 303 |
+
overflow-y: auto;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.modal-container,
|
| 307 |
+
.modal-content {
|
| 308 |
+
max-width: calc(100vw - 40px);
|
| 309 |
+
max-height: calc(100vh - 40px);
|
| 310 |
+
margin: 20px auto;
|
| 311 |
+
overflow-y: auto;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
@media (max-width: 768px) {
|
| 315 |
+
|
| 316 |
+
.modal-container,
|
| 317 |
+
.modal-content {
|
| 318 |
+
max-width: calc(100vw - 20px);
|
| 319 |
+
max-height: calc(100vh - 20px);
|
| 320 |
+
margin: 10px;
|
| 321 |
+
border-radius: 12px;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.modal-close,
|
| 325 |
+
.btn-close {
|
| 326 |
+
width: 44px;
|
| 327 |
+
height: 44px;
|
| 328 |
+
font-size: 24px;
|
| 329 |
+
}
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
/* ==================== CARDS EXCEEDING VIEWPORT HEIGHT ==================== */
|
| 333 |
+
.card,
|
| 334 |
+
.result-card,
|
| 335 |
+
.analysis-card,
|
| 336 |
+
.verdict-card,
|
| 337 |
+
.metric-card {
|
| 338 |
+
max-height: calc(100vh - 100px);
|
| 339 |
+
overflow-y: auto;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
@media (max-width: 768px) {
|
| 343 |
+
|
| 344 |
+
.card,
|
| 345 |
+
.result-card,
|
| 346 |
+
.analysis-card {
|
| 347 |
+
max-height: none;
|
| 348 |
+
height: auto;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.verdict-card {
|
| 352 |
+
padding: 20px 16px;
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
/* ==================== HISTORY / FILTER PANEL HEIGHT FIXES ==================== */
|
| 357 |
+
.history-controls {
|
| 358 |
+
max-height: calc(100vh - 200px);
|
| 359 |
+
overflow-y: auto;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
@media (max-width: 768px) {
|
| 363 |
+
.history-controls {
|
| 364 |
+
max-height: none;
|
| 365 |
+
height: auto;
|
| 366 |
+
overflow-y: visible;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.filter-controls {
|
| 370 |
+
flex-direction: column;
|
| 371 |
+
gap: 10px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.filter-select {
|
| 375 |
+
width: 100%;
|
| 376 |
+
min-width: 100%;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.export-controls {
|
| 380 |
+
flex-direction: column;
|
| 381 |
+
gap: 10px;
|
| 382 |
+
width: 100%;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.btn-export,
|
| 386 |
+
.btn-clear-all {
|
| 387 |
+
width: 100%;
|
| 388 |
+
min-height: 48px;
|
| 389 |
+
}
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
/* ==================== HISTORY TABLE CONSTRAINTS ==================== */
|
| 393 |
+
@media (max-width: 768px) {
|
| 394 |
+
.history-table-container {
|
| 395 |
+
max-height: calc(100vh - 350px);
|
| 396 |
+
overflow-x: auto;
|
| 397 |
+
overflow-y: auto;
|
| 398 |
+
-webkit-overflow-scrolling: touch;
|
| 399 |
+
}
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
@media (max-width: 640px) {
|
| 403 |
+
.history-table {
|
| 404 |
+
min-width: 600px;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
.table-filename {
|
| 408 |
+
max-width: 100px;
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
/* Floating buttons — only apply to actual UI floating buttons, not decorative */
|
| 413 |
+
@media (max-width: 768px) {
|
| 414 |
+
|
| 415 |
+
.floating-button,
|
| 416 |
+
.btn-floating {
|
| 417 |
+
position: fixed;
|
| 418 |
+
bottom: 20px !important;
|
| 419 |
+
right: 20px !important;
|
| 420 |
+
width: 56px;
|
| 421 |
+
height: 56px;
|
| 422 |
+
max-width: calc(100vw - 40px);
|
| 423 |
+
}
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
/* ==================== HERO SECTION SHORT SCREENS ==================== */
|
| 427 |
+
@media (max-height: 700px) {
|
| 428 |
+
.hero {
|
| 429 |
+
min-height: auto;
|
| 430 |
+
padding: 80px 0 40px;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.hero-title {
|
| 434 |
+
font-size: 40px;
|
| 435 |
+
margin-bottom: 15px;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
.hero-description {
|
| 439 |
+
font-size: 15px;
|
| 440 |
+
margin-bottom: 20px;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.hero-stats {
|
| 444 |
+
margin-top: 20px;
|
| 445 |
+
gap: 15px;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.hero-actions {
|
| 449 |
+
gap: 10px;
|
| 450 |
+
margin-top: 20px;
|
| 451 |
+
}
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
/* ==================== ULTRA-WIDE SCREENS (2560px+) ==================== */
|
| 455 |
+
@media (min-width: 2560px) {
|
| 456 |
+
.container {
|
| 457 |
+
max-width: 1800px;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.analysis-container {
|
| 461 |
+
max-width: 1600px;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
.hero-content {
|
| 465 |
+
max-width: 60%;
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
/* ==================== EXTREME ASPECT RATIOS (21:9, 32:9) ==================== */
|
| 470 |
+
@media (min-aspect-ratio: 21/9) {
|
| 471 |
+
.hero-content {
|
| 472 |
+
max-width: 55%;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
.analysis-grid {
|
| 476 |
+
gap: 60px;
|
| 477 |
+
}
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
/* ==================== LANDSCAPE MOBILE (SHORT & WIDE) ==================== */
|
| 481 |
+
@media (max-height: 500px) and (orientation: landscape) {
|
| 482 |
+
.navbar {
|
| 483 |
+
padding: 6px 0;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
.analysis-container {
|
| 487 |
+
padding-top: 70px;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.hero {
|
| 491 |
+
min-height: auto;
|
| 492 |
+
padding: 50px 0 30px;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.hero-title {
|
| 496 |
+
font-size: 28px;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.section-title {
|
| 500 |
+
font-size: 24px;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
.upload-area {
|
| 504 |
+
min-height: 180px;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
.preview-area {
|
| 508 |
+
max-height: 250px;
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
/* Force single column even in landscape if screen is too short */
|
| 512 |
+
.analysis-grid {
|
| 513 |
+
grid-template-columns: 1fr;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
.statistics-grid {
|
| 517 |
+
grid-template-columns: repeat(2, 1fr);
|
| 518 |
+
}
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
/* ==================== VERY TALL MOBILE SCREENS ==================== */
|
| 522 |
+
@media (min-height: 900px) and (max-width: 480px) {
|
| 523 |
+
|
| 524 |
+
/* Optimize for very tall phones */
|
| 525 |
+
.upload-area {
|
| 526 |
+
min-height: 450px;
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
.preview-area {
|
| 530 |
+
max-height: 500px;
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
.section {
|
| 534 |
+
padding: 80px 0;
|
| 535 |
+
}
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
/* ==================== PWA STANDALONE MODE ==================== */
|
| 539 |
+
@media (display-mode: standalone) {
|
| 540 |
+
body {
|
| 541 |
+
overscroll-behavior-y: contain;
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
.navbar {
|
| 545 |
+
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
.footer {
|
| 549 |
+
padding-bottom: max(40px, calc(40px + env(safe-area-inset-bottom)));
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
/* Prevent content from hiding behind home indicator */
|
| 553 |
+
.action-panel,
|
| 554 |
+
.bottom-sheet,
|
| 555 |
+
.queue-footer {
|
| 556 |
+
padding-bottom: max(12px, calc(12px + env(safe-area-inset-bottom)));
|
| 557 |
+
}
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
/* ==================== EMPTY STATES ==================== */
|
| 561 |
+
.empty-state {
|
| 562 |
+
max-width: 100%;
|
| 563 |
+
padding: 40px 20px;
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
@media (max-width: 768px) {
|
| 567 |
+
.empty-state {
|
| 568 |
+
padding: 30px 16px;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
.empty-icon {
|
| 572 |
+
font-size: 40px;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
.empty-state h3 {
|
| 576 |
+
font-size: 18px;
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
.empty-state p {
|
| 580 |
+
font-size: 14px;
|
| 581 |
+
}
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
/* ==================== GRID LAYOUTS MOBILE OPTIMIZATION ==================== */
|
| 585 |
+
@media (max-width: 768px) {
|
| 586 |
+
|
| 587 |
+
.tech-grid,
|
| 588 |
+
.showcase-grid,
|
| 589 |
+
.capabilities-grid {
|
| 590 |
+
grid-template-columns: 1fr;
|
| 591 |
+
gap: 16px;
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
.model-stats-grid {
|
| 595 |
+
grid-template-columns: repeat(2, 1fr);
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
.recent-grid {
|
| 599 |
+
grid-template-columns: 1fr;
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
.pipeline {
|
| 603 |
+
flex-direction: column;
|
| 604 |
+
gap: 20px;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
/* RESPONSIVE: Hide pipeline arrows on mobile since steps stack */
|
| 608 |
+
.pipeline-arrow {
|
| 609 |
+
display: none;
|
| 610 |
+
}
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
/* ==================== TOUCH TARGET SIZES (WCAG AAA) — Mobile Only ==================== */
|
| 614 |
+
@media (max-width: 768px) {
|
| 615 |
+
|
| 616 |
+
button,
|
| 617 |
+
.btn,
|
| 618 |
+
.btn-primary,
|
| 619 |
+
.btn-secondary,
|
| 620 |
+
.btn-hero-primary,
|
| 621 |
+
.btn-hero-secondary,
|
| 622 |
+
.btn-upload,
|
| 623 |
+
.btn-export,
|
| 624 |
+
.btn-toggle,
|
| 625 |
+
.btn-page,
|
| 626 |
+
.btn-batch,
|
| 627 |
+
.control-btn,
|
| 628 |
+
[role="button"] {
|
| 629 |
+
min-height: 44px;
|
| 630 |
+
min-width: 44px;
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
/* Checkboxes and radios need larger hit area */
|
| 634 |
+
[type="checkbox"],
|
| 635 |
+
[type="radio"] {
|
| 636 |
+
min-height: 24px;
|
| 637 |
+
min-width: 24px;
|
| 638 |
+
}
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
/* ==================== PREVENT iOS ZOOM ON INPUT FOCUS ==================== */
|
| 642 |
+
@media (max-width: 768px) {
|
| 643 |
+
|
| 644 |
+
input,
|
| 645 |
+
select,
|
| 646 |
+
textarea {
|
| 647 |
+
font-size: 16px !important;
|
| 648 |
+
}
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
/* ==================== BETTER TAP HIGHLIGHTING ==================== */
|
| 652 |
+
* {
|
| 653 |
+
-webkit-tap-highlight-color: rgba(227, 245, 20, 0.15);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
/* ==================== SAFE SCROLLING ==================== */
|
| 657 |
+
.scrollable {
|
| 658 |
+
-webkit-overflow-scrolling: touch;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
/* ==================== ENSURE BUTTONS DONT WRAP TEXT ==================== */
|
| 662 |
+
@media (max-width: 480px) {
|
| 663 |
+
|
| 664 |
+
.btn-primary,
|
| 665 |
+
.btn-secondary,
|
| 666 |
+
.btn-hero-primary,
|
| 667 |
+
.btn-hero-secondary {
|
| 668 |
+
font-size: 14px;
|
| 669 |
+
padding: 12px 20px;
|
| 670 |
+
white-space: nowrap;
|
| 671 |
+
overflow: hidden;
|
| 672 |
+
text-overflow: ellipsis;
|
| 673 |
+
}
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
/* ==================== STATISTICS SECTION MOBILE ==================== */
|
| 677 |
+
@media (max-width: 480px) {
|
| 678 |
+
.statistics-grid {
|
| 679 |
+
grid-template-columns: 1fr !important;
|
| 680 |
+
gap: 12px;
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
.stat-card {
|
| 684 |
+
padding: 16px;
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
.stat-value {
|
| 688 |
+
font-size: 24px;
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
.stat-label {
|
| 692 |
+
font-size: 12px;
|
| 693 |
+
}
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
/* ==================== CONTENT PADDING TOP/BOTTOM FIXES ==================== */
|
| 697 |
+
@media (max-width: 768px) {
|
| 698 |
+
section {
|
| 699 |
+
padding: 50px 0;
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
.analysis-container,
|
| 703 |
+
.history-container {
|
| 704 |
+
padding-top: 100px;
|
| 705 |
+
padding-bottom: 40px;
|
| 706 |
+
}
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
@media (max-width: 480px) {
|
| 710 |
+
section {
|
| 711 |
+
padding: 40px 0;
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
.analysis-container,
|
| 715 |
+
.history-container {
|
| 716 |
+
padding-top: 90px;
|
| 717 |
+
padding-bottom: 30px;
|
| 718 |
+
}
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
/* ==================== VIDEO PLAYER RESPONSIVE ==================== */
|
| 722 |
+
.video-container,
|
| 723 |
+
.video-window-container,
|
| 724 |
+
.video-content-wrapper {
|
| 725 |
+
max-width: 100%;
|
| 726 |
+
overflow: hidden;
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
.video-container video,
|
| 730 |
+
.demo-video {
|
| 731 |
+
max-width: 100%;
|
| 732 |
+
height: auto;
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
@media (max-width: 768px) {
|
| 736 |
+
.img-comp-container {
|
| 737 |
+
height: 300px !important;
|
| 738 |
+
}
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
@media (max-width: 480px) {
|
| 742 |
+
.img-comp-container {
|
| 743 |
+
height: 250px !important;
|
| 744 |
+
}
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
/* ==================== ORBIT/FEATURES SECTION ==================== */
|
| 748 |
+
@media (max-width: 768px) {
|
| 749 |
+
.orbit-container {
|
| 750 |
+
min-height: 400px;
|
| 751 |
+
overflow: hidden;
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
.orbit-stage {
|
| 755 |
+
transform: scale(0.7);
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
.orbit-info-card {
|
| 759 |
+
max-width: 90%;
|
| 760 |
+
padding: 16px;
|
| 761 |
+
}
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
@media (max-width: 480px) {
|
| 765 |
+
.orbit-stage {
|
| 766 |
+
transform: scale(0.5);
|
| 767 |
+
}
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
/* ==================== EXTENSION SECTION ==================== */
|
| 771 |
+
@media (max-width: 768px) {
|
| 772 |
+
.extension-container {
|
| 773 |
+
flex-direction: column;
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
.extension-content,
|
| 777 |
+
.extension-visual {
|
| 778 |
+
width: 100%;
|
| 779 |
+
}
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
+
/* ==================== OVERFLOW PREVENTION ==================== */
|
| 783 |
+
/* Applied to specific layout containers only — not globally */
|
| 784 |
+
.container,
|
| 785 |
+
.analysis-grid,
|
| 786 |
+
.features-grid,
|
| 787 |
+
.tech-grid,
|
| 788 |
+
.pipeline-grid {
|
| 789 |
+
max-width: 100%;
|
| 790 |
+
overflow-x: hidden;
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
/* ==================== BATCH ACTIONS BAR MOBILE ==================== */
|
| 794 |
+
@media (max-width: 768px) {
|
| 795 |
+
.batch-actions-bar {
|
| 796 |
+
width: 90%;
|
| 797 |
+
max-width: 400px;
|
| 798 |
+
bottom: 20px;
|
| 799 |
+
padding: 12px 16px;
|
| 800 |
+
flex-direction: column;
|
| 801 |
+
gap: 10px;
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
.btn-batch {
|
| 805 |
+
width: 100%;
|
| 806 |
+
min-height: 44px;
|
| 807 |
+
}
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
/* ==================== PAGINATION MOBILE ==================== */
|
| 811 |
+
@media (max-width: 768px) {
|
| 812 |
+
.pagination {
|
| 813 |
+
gap: 8px;
|
| 814 |
+
padding-bottom: 40px;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
.btn-page {
|
| 818 |
+
width: 40px;
|
| 819 |
+
height: 40px;
|
| 820 |
+
font-size: 14px;
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
.page-info {
|
| 824 |
+
font-size: 13px;
|
| 825 |
+
}
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
/* ==================== FIX ANY FIXED POSITIONING OVERFLOW ==================== */
|
| 829 |
+
@media (max-width: 768px) {
|
| 830 |
+
|
| 831 |
+
[style*="position: fixed"],
|
| 832 |
+
[style*="position:fixed"] {
|
| 833 |
+
max-width: 100vw;
|
| 834 |
+
}
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
/* ==================== LANDSCAPE TABLET ==================== */
|
| 838 |
+
@media (min-width: 769px) and (max-width: 1024px) and (orientation: landscape) {
|
| 839 |
+
.analysis-grid {
|
| 840 |
+
grid-template-columns: 1fr 1fr;
|
| 841 |
+
gap: 30px;
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
.upload-area {
|
| 845 |
+
min-height: 350px;
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
.preview-area {
|
| 849 |
+
max-height: 400px;
|
| 850 |
+
}
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
/* ==================== PORTRAIT TABLET ==================== */
|
| 854 |
+
@media (min-width: 769px) and (max-width: 1024px) and (orientation: portrait) {
|
| 855 |
+
.analysis-grid {
|
| 856 |
+
grid-template-columns: 1fr;
|
| 857 |
+
gap: 30px;
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
.container {
|
| 861 |
+
padding: 0 30px;
|
| 862 |
+
}
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
/* ==================== ENSURE TEXT DOESNT OVERFLOW ==================== */
|
| 866 |
+
h1,
|
| 867 |
+
h2,
|
| 868 |
+
h3,
|
| 869 |
+
h4,
|
| 870 |
+
h5,
|
| 871 |
+
h6,
|
| 872 |
+
p,
|
| 873 |
+
span,
|
| 874 |
+
div {
|
| 875 |
+
word-wrap: break-word;
|
| 876 |
+
overflow-wrap: break-word;
|
| 877 |
+
}
|
| 878 |
+
|
| 879 |
+
/* ==================== FINAL SAFETY NET ==================== */
|
| 880 |
+
@media (max-width: 768px) {
|
| 881 |
+
|
| 882 |
+
/* Ensure absolutely nothing causes horizontal scroll */
|
| 883 |
+
* {
|
| 884 |
+
max-width: 100vw !important;
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
/* Exception for background elements */
|
| 888 |
+
.mesh-background,
|
| 889 |
+
#particles-js,
|
| 890 |
+
#canvas-container,
|
| 891 |
+
.noise-overlay,
|
| 892 |
+
.gradient-orb,
|
| 893 |
+
.floating-3d-object,
|
| 894 |
+
body,
|
| 895 |
+
html {
|
| 896 |
+
max-width: none !important;
|
| 897 |
+
}
|
| 898 |
+
}
|
frontend/responsive-pages.css
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ==================================================================================
|
| 2 |
+
RESPONSIVE STYLES FOR NON-LANDING PAGES
|
| 3 |
+
==================================================================================
|
| 4 |
+
This file contains responsive CSS for: analysis.html, history.html, video_result.html
|
| 5 |
+
DO NOT link this file in index.html - landing page should keep its original design
|
| 6 |
+
================================================================================== */
|
| 7 |
+
|
| 8 |
+
/* ==================== FLUID TYPOGRAPHY VARIABLES ==================== */
|
| 9 |
+
:root {
|
| 10 |
+
/* Fluid font sizes for inner pages */
|
| 11 |
+
--font-size-page-title: clamp(1.75rem, 4vw + 0.5rem, 3rem);
|
| 12 |
+
--font-size-section-header: clamp(1.25rem, 2.5vw + 0.5rem, 2rem);
|
| 13 |
+
--font-size-card-title: clamp(1rem, 1.5vw + 0.5rem, 1.375rem);
|
| 14 |
+
--font-size-body: clamp(0.875rem, 1vw + 0.5rem, 1.125rem);
|
| 15 |
+
--font-size-small: clamp(0.75rem, 0.5vw + 0.5rem, 0.875rem);
|
| 16 |
+
|
| 17 |
+
/* Fluid spacing */
|
| 18 |
+
--container-padding: clamp(16px, 4vw, 40px);
|
| 19 |
+
--card-padding: clamp(16px, 3vw, 32px);
|
| 20 |
+
--gap-sm: clamp(12px, 2vw, 20px);
|
| 21 |
+
--gap-md: clamp(20px, 3vw, 40px);
|
| 22 |
+
--gap-lg: clamp(30px, 5vw, 60px);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/* ==================== ANALYSIS PAGE RESPONSIVE ==================== */
|
| 26 |
+
|
| 27 |
+
/* Analysis grid - stack on tablet and below */
|
| 28 |
+
.analysis-grid {
|
| 29 |
+
gap: var(--gap-md);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
@media (max-width: 1024px) {
|
| 33 |
+
.analysis-grid {
|
| 34 |
+
grid-template-columns: 1fr !important;
|
| 35 |
+
gap: var(--gap-md);
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/* Upload section responsive */
|
| 40 |
+
.upload-section {
|
| 41 |
+
gap: var(--gap-sm);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.section-header-small h2 {
|
| 45 |
+
font-size: var(--font-size-section-header);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/* Upload area fluid sizing */
|
| 49 |
+
.upload-area {
|
| 50 |
+
min-height: clamp(250px, 35vh, 400px);
|
| 51 |
+
padding: var(--card-padding);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
@media (max-width: 768px) {
|
| 55 |
+
.upload-area {
|
| 56 |
+
min-height: 220px;
|
| 57 |
+
border-radius: 16px;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.upload-icon {
|
| 61 |
+
font-size: 48px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.upload-text {
|
| 65 |
+
font-size: 14px;
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/* Results section responsive */
|
| 70 |
+
.results-section {
|
| 71 |
+
padding: var(--card-padding);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
@media (max-width: 768px) {
|
| 75 |
+
.results-section {
|
| 76 |
+
border-radius: 16px;
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/* Statistics grid responsive */
|
| 81 |
+
.statistics-grid {
|
| 82 |
+
gap: var(--gap-sm);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
@media (max-width: 768px) {
|
| 86 |
+
.statistics-grid {
|
| 87 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
@media (max-width: 480px) {
|
| 92 |
+
.statistics-grid {
|
| 93 |
+
grid-template-columns: 1fr !important;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.stat-card {
|
| 97 |
+
padding: 16px;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.stat-value {
|
| 101 |
+
font-size: 24px;
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/* Recent analyses grid responsive */
|
| 106 |
+
.recent-grid {
|
| 107 |
+
gap: var(--gap-sm);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
@media (max-width: 768px) {
|
| 111 |
+
.recent-grid {
|
| 112 |
+
grid-template-columns: 1fr !important;
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/* ==================== HISTORY PAGE RESPONSIVE ==================== */
|
| 117 |
+
|
| 118 |
+
/* History controls fluid spacing */
|
| 119 |
+
.history-controls {
|
| 120 |
+
gap: var(--gap-sm);
|
| 121 |
+
padding: var(--card-padding);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
@media (max-width: 768px) {
|
| 125 |
+
.history-controls {
|
| 126 |
+
flex-direction: column;
|
| 127 |
+
align-items: stretch;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.search-container {
|
| 131 |
+
min-width: 100%;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.filter-controls {
|
| 135 |
+
flex-direction: column;
|
| 136 |
+
width: 100%;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.filter-select {
|
| 140 |
+
width: 100%;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.export-controls {
|
| 144 |
+
flex-direction: column;
|
| 145 |
+
width: 100%;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.btn-export,
|
| 149 |
+
.btn-clear-all {
|
| 150 |
+
width: 100%;
|
| 151 |
+
min-height: 48px;
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/* History table horizontal scroll on mobile */
|
| 156 |
+
.history-table-container {
|
| 157 |
+
overflow-x: auto;
|
| 158 |
+
-webkit-overflow-scrolling: touch;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
@media (max-width: 768px) {
|
| 162 |
+
.history-table {
|
| 163 |
+
min-width: 600px;
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
/* Grid view card sizing */
|
| 168 |
+
@media (max-width: 768px) {
|
| 169 |
+
.history-grid {
|
| 170 |
+
grid-template-columns: 1fr !important;
|
| 171 |
+
gap: var(--gap-sm);
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/* Batch actions bar mobile */
|
| 176 |
+
@media (max-width: 768px) {
|
| 177 |
+
.batch-actions {
|
| 178 |
+
flex-wrap: wrap;
|
| 179 |
+
gap: 10px;
|
| 180 |
+
padding: 12px;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.batch-actions button {
|
| 184 |
+
flex: 1;
|
| 185 |
+
min-width: calc(50% - 10px);
|
| 186 |
+
min-height: 44px;
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
/* ==================== VIDEO RESULT PAGE RESPONSIVE ==================== */
|
| 191 |
+
|
| 192 |
+
/* Video player container responsive */
|
| 193 |
+
.video-preview-container {
|
| 194 |
+
max-width: 100%;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
@media (max-width: 768px) {
|
| 198 |
+
.video-window-container {
|
| 199 |
+
border-radius: 16px;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.window-header {
|
| 203 |
+
padding: 10px 12px;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.play-button {
|
| 207 |
+
width: 60px;
|
| 208 |
+
height: 60px;
|
| 209 |
+
font-size: 28px;
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
@media (max-width: 480px) {
|
| 214 |
+
.play-button {
|
| 215 |
+
width: 50px;
|
| 216 |
+
height: 50px;
|
| 217 |
+
font-size: 24px;
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
/* Dashboard grid stack on mobile */
|
| 222 |
+
.dashboard-grid {
|
| 223 |
+
gap: var(--gap-md);
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
@media (max-width: 1024px) {
|
| 227 |
+
.dashboard-grid {
|
| 228 |
+
grid-template-columns: 1fr !important;
|
| 229 |
+
}
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/* Stats grid compact on mobile */
|
| 233 |
+
@media (max-width: 768px) {
|
| 234 |
+
.video-stats-grid {
|
| 235 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 236 |
+
gap: var(--gap-sm);
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
@media (max-width: 480px) {
|
| 241 |
+
.video-stats-grid {
|
| 242 |
+
grid-template-columns: 1fr !important;
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
/* Timeline chart container responsive */
|
| 247 |
+
.timeline-container,
|
| 248 |
+
.chart-container {
|
| 249 |
+
max-width: 100%;
|
| 250 |
+
overflow-x: auto;
|
| 251 |
+
-webkit-overflow-scrolling: touch;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
/* Frame grid responsive */
|
| 255 |
+
@media (max-width: 768px) {
|
| 256 |
+
.frame-grid {
|
| 257 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 258 |
+
gap: var(--gap-sm);
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
@media (max-width: 480px) {
|
| 263 |
+
.frame-grid {
|
| 264 |
+
grid-template-columns: 1fr !important;
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
/* Report section mobile */
|
| 269 |
+
@media (max-width: 768px) {
|
| 270 |
+
.report-section {
|
| 271 |
+
padding: var(--card-padding);
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.report-header {
|
| 275 |
+
flex-direction: column;
|
| 276 |
+
gap: 12px;
|
| 277 |
+
align-items: flex-start;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.btn-download-report {
|
| 281 |
+
width: 100%;
|
| 282 |
+
min-height: 48px;
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
/* ==================== SHARED RESPONSIVE PATTERNS ==================== */
|
| 287 |
+
|
| 288 |
+
/* Container padding */
|
| 289 |
+
.analysis-container,
|
| 290 |
+
.history-container,
|
| 291 |
+
.video-result-container {
|
| 292 |
+
padding-left: var(--container-padding);
|
| 293 |
+
padding-right: var(--container-padding);
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
/* Card styling consistency */
|
| 297 |
+
.result-card,
|
| 298 |
+
.analysis-card,
|
| 299 |
+
.history-card,
|
| 300 |
+
.video-card {
|
| 301 |
+
padding: var(--card-padding);
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
@media (max-width: 768px) {
|
| 305 |
+
|
| 306 |
+
.result-card,
|
| 307 |
+
.analysis-card,
|
| 308 |
+
.history-card,
|
| 309 |
+
.video-card {
|
| 310 |
+
border-radius: 16px;
|
| 311 |
+
}
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
/* Touch targets for interactive elements */
|
| 315 |
+
@media (max-width: 768px) {
|
| 316 |
+
|
| 317 |
+
button,
|
| 318 |
+
.btn,
|
| 319 |
+
.btn-primary,
|
| 320 |
+
.btn-secondary,
|
| 321 |
+
[role="button"] {
|
| 322 |
+
min-height: 44px;
|
| 323 |
+
min-width: 44px;
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
/* Prevent iOS input zoom */
|
| 328 |
+
@media (max-width: 768px) {
|
| 329 |
+
|
| 330 |
+
input,
|
| 331 |
+
select,
|
| 332 |
+
textarea {
|
| 333 |
+
font-size: 16px !important;
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
/* Modal responsive */
|
| 338 |
+
@media (max-width: 768px) {
|
| 339 |
+
.modal-content {
|
| 340 |
+
max-width: calc(100vw - 32px);
|
| 341 |
+
max-height: calc(100vh - 32px);
|
| 342 |
+
margin: 16px;
|
| 343 |
+
border-radius: 16px;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.modal-close {
|
| 347 |
+
width: 44px;
|
| 348 |
+
height: 44px;
|
| 349 |
+
}
|
| 350 |
+
}
|
frontend/script.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/scroll_indicator.css
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ==================== SCROLL INDICATOR ==================== */
|
| 2 |
+
.scroll-indicator {
|
| 3 |
+
position: absolute;
|
| 4 |
+
bottom: 30px;
|
| 5 |
+
left: 50%;
|
| 6 |
+
transform: translateX(-50%);
|
| 7 |
+
display: flex;
|
| 8 |
+
flex-direction: column;
|
| 9 |
+
align-items: center;
|
| 10 |
+
gap: 8px;
|
| 11 |
+
z-index: 10;
|
| 12 |
+
opacity: 0.7;
|
| 13 |
+
transition: opacity 0.3s ease;
|
| 14 |
+
cursor: pointer;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.scroll-indicator:hover {
|
| 18 |
+
opacity: 1;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.mouse {
|
| 22 |
+
width: 26px;
|
| 23 |
+
height: 42px;
|
| 24 |
+
border: 2px solid rgba(255, 255, 255, 0.4);
|
| 25 |
+
border-radius: 20px;
|
| 26 |
+
position: relative;
|
| 27 |
+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.wheel {
|
| 31 |
+
width: 4px;
|
| 32 |
+
height: 8px;
|
| 33 |
+
background: var(--accent-yellow, #E3F514);
|
| 34 |
+
border-radius: 2px;
|
| 35 |
+
position: absolute;
|
| 36 |
+
top: 6px;
|
| 37 |
+
left: 50%;
|
| 38 |
+
transform: translateX(-50%);
|
| 39 |
+
animation: scrollWheel 2s ease-in-out infinite;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.arrow-scroll {
|
| 43 |
+
width: 10px;
|
| 44 |
+
height: 10px;
|
| 45 |
+
border-right: 2px solid rgba(255, 255, 255, 0.4);
|
| 46 |
+
border-bottom: 2px solid rgba(255, 255, 255, 0.4);
|
| 47 |
+
transform: rotate(45deg);
|
| 48 |
+
animation: scrollArrow 2s ease-in-out infinite;
|
| 49 |
+
animation-delay: 0.2s;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
@keyframes scrollWheel {
|
| 53 |
+
0% {
|
| 54 |
+
top: 6px;
|
| 55 |
+
opacity: 1;
|
| 56 |
+
height: 8px;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
100% {
|
| 60 |
+
top: 24px;
|
| 61 |
+
opacity: 0;
|
| 62 |
+
height: 4px;
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
@keyframes scrollArrow {
|
| 67 |
+
0% {
|
| 68 |
+
transform: rotate(45deg) translate(0, 0);
|
| 69 |
+
opacity: 0;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
50% {
|
| 73 |
+
opacity: 1;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
100% {
|
| 77 |
+
transform: rotate(45deg) translate(2px, 2px);
|
| 78 |
+
opacity: 0;
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/* Reduced Motion */
|
| 83 |
+
@media (prefers-reduced-motion: reduce) {
|
| 84 |
+
|
| 85 |
+
.wheel,
|
| 86 |
+
.arrow-scroll {
|
| 87 |
+
animation: none;
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
/* Hide on small screens where height is limited */
|
| 92 |
+
@media (max-height: 700px) {
|
| 93 |
+
.scroll-indicator {
|
| 94 |
+
display: none;
|
| 95 |
+
}
|
| 96 |
+
}
|
frontend/service-worker.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* DeepGuard Service Worker
|
| 3 |
+
* Handles offline functionality, caching, and PWA features
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
const CACHE_VERSION = 'v1.0.0';
|
| 7 |
+
const CACHE_NAME = `deepguard-${CACHE_VERSION}`;
|
| 8 |
+
|
| 9 |
+
// Assets to cache immediately on install
|
| 10 |
+
const STATIC_ASSETS = [
|
| 11 |
+
'/',
|
| 12 |
+
'/index.html',
|
| 13 |
+
'/analysis.html',
|
| 14 |
+
'/history.html',
|
| 15 |
+
'/offline.html',
|
| 16 |
+
'/style.css',
|
| 17 |
+
'/animations.css',
|
| 18 |
+
'/history.css',
|
| 19 |
+
'/loader.css',
|
| 20 |
+
'/orbit.css',
|
| 21 |
+
'/showcase.css',
|
| 22 |
+
'/video_player.css',
|
| 23 |
+
'/extension.css',
|
| 24 |
+
'/scroll_indicator.css',
|
| 25 |
+
'/script.js',
|
| 26 |
+
'/mobile.js',
|
| 27 |
+
'/loader.js',
|
| 28 |
+
'/hero_reveal.js',
|
| 29 |
+
'/motion.js',
|
| 30 |
+
'/orbit_interaction.js',
|
| 31 |
+
'/three_bg.js',
|
| 32 |
+
'/logo.ico',
|
| 33 |
+
'/icon-192.png',
|
| 34 |
+
'/icon-512.png',
|
| 35 |
+
'/manifest.json'
|
| 36 |
+
];
|
| 37 |
+
|
| 38 |
+
// Assets that can be cached on demand
|
| 39 |
+
const RUNTIME_CACHE = 'deepguard-runtime';
|
| 40 |
+
|
| 41 |
+
// Install event - cache static assets
|
| 42 |
+
self.addEventListener('install', (event) => {
|
| 43 |
+
console.log('[Service Worker] Installing...');
|
| 44 |
+
|
| 45 |
+
event.waitUntil(
|
| 46 |
+
caches.open(CACHE_NAME)
|
| 47 |
+
.then((cache) => {
|
| 48 |
+
console.log('[Service Worker] Caching static assets');
|
| 49 |
+
return cache.addAll(STATIC_ASSETS);
|
| 50 |
+
})
|
| 51 |
+
.then(() => {
|
| 52 |
+
console.log('[Service Worker] Installation complete');
|
| 53 |
+
return self.skipWaiting(); // Activate immediately
|
| 54 |
+
})
|
| 55 |
+
.catch((error) => {
|
| 56 |
+
console.error('[Service Worker] Installation failed:', error);
|
| 57 |
+
})
|
| 58 |
+
);
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
// Activate event - clean up old caches
|
| 62 |
+
self.addEventListener('activate', (event) => {
|
| 63 |
+
console.log('[Service Worker] Activating...');
|
| 64 |
+
|
| 65 |
+
event.waitUntil(
|
| 66 |
+
caches.keys()
|
| 67 |
+
.then((cacheNames) => {
|
| 68 |
+
return Promise.all(
|
| 69 |
+
cacheNames
|
| 70 |
+
.filter((name) => name.startsWith('deepguard-') && name !== CACHE_NAME)
|
| 71 |
+
.map((name) => {
|
| 72 |
+
console.log('[Service Worker] Deleting old cache:', name);
|
| 73 |
+
return caches.delete(name);
|
| 74 |
+
})
|
| 75 |
+
);
|
| 76 |
+
})
|
| 77 |
+
.then(() => {
|
| 78 |
+
console.log('[Service Worker] Activation complete');
|
| 79 |
+
return self.clients.claim(); // Take control immediately
|
| 80 |
+
})
|
| 81 |
+
);
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
// Fetch event - serve from cache, fallback to network
|
| 85 |
+
self.addEventListener('fetch', (event) => {
|
| 86 |
+
const { request } = event;
|
| 87 |
+
const url = new URL(request.url);
|
| 88 |
+
|
| 89 |
+
// Skip cross-origin requests
|
| 90 |
+
if (url.origin !== location.origin) {
|
| 91 |
+
return;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Skip API requests (let them go to network)
|
| 95 |
+
if (url.pathname.startsWith('/api/')) {
|
| 96 |
+
return;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
event.respondWith(
|
| 100 |
+
caches.match(request)
|
| 101 |
+
.then((cachedResponse) => {
|
| 102 |
+
if (cachedResponse) {
|
| 103 |
+
console.log('[Service Worker] Serving from cache:', request.url);
|
| 104 |
+
return cachedResponse;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Not in cache, fetch from network
|
| 108 |
+
return fetch(request)
|
| 109 |
+
.then((response) => {
|
| 110 |
+
// Don't cache non-successful responses
|
| 111 |
+
if (!response || response.status !== 200 || response.type !== 'basic') {
|
| 112 |
+
return response;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Clone response for caching
|
| 116 |
+
const responseToCache = response.clone();
|
| 117 |
+
|
| 118 |
+
// Cache runtime assets
|
| 119 |
+
caches.open(RUNTIME_CACHE)
|
| 120 |
+
.then((cache) => {
|
| 121 |
+
cache.put(request, responseToCache);
|
| 122 |
+
});
|
| 123 |
+
|
| 124 |
+
return response;
|
| 125 |
+
})
|
| 126 |
+
.catch((error) => {
|
| 127 |
+
console.error('[Service Worker] Fetch failed:', error);
|
| 128 |
+
|
| 129 |
+
// Return offline page for navigation requests
|
| 130 |
+
if (request.mode === 'navigate') {
|
| 131 |
+
return caches.match('/offline.html');
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// Return fallback for images
|
| 135 |
+
if (request.destination === 'image') {
|
| 136 |
+
return caches.match('/logo.svg');
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
return new Response('Offline - content not available', {
|
| 140 |
+
status: 503,
|
| 141 |
+
statusText: 'Service Unavailable',
|
| 142 |
+
headers: new Headers({
|
| 143 |
+
'Content-Type': 'text/plain'
|
| 144 |
+
})
|
| 145 |
+
});
|
| 146 |
+
});
|
| 147 |
+
})
|
| 148 |
+
);
|
| 149 |
+
});
|
| 150 |
+
|
| 151 |
+
// Background sync for offline analysis (future enhancement)
|
| 152 |
+
self.addEventListener('sync', (event) => {
|
| 153 |
+
console.log('[Service Worker] Background sync:', event.tag);
|
| 154 |
+
|
| 155 |
+
if (event.tag === 'sync-analyses') {
|
| 156 |
+
event.waitUntil(syncPendingAnalyses());
|
| 157 |
+
}
|
| 158 |
+
});
|
| 159 |
+
|
| 160 |
+
// Push notifications (future enhancement)
|
| 161 |
+
self.addEventListener('push', (event) => {
|
| 162 |
+
console.log('[Service Worker] Push notification received');
|
| 163 |
+
|
| 164 |
+
const data = event.data ? event.data.json() : {};
|
| 165 |
+
const title = data.title || 'DeepGuard';
|
| 166 |
+
const options = {
|
| 167 |
+
body: data.body || 'Analysis complete',
|
| 168 |
+
icon: '/icon-192.png',
|
| 169 |
+
badge: '/icon-192.png',
|
| 170 |
+
vibrate: [200, 100, 200],
|
| 171 |
+
data: {
|
| 172 |
+
url: data.url || '/history.html'
|
| 173 |
+
}
|
| 174 |
+
};
|
| 175 |
+
|
| 176 |
+
event.waitUntil(
|
| 177 |
+
self.registration.showNotification(title, options)
|
| 178 |
+
);
|
| 179 |
+
});
|
| 180 |
+
|
| 181 |
+
// Notification click handler
|
| 182 |
+
self.addEventListener('notificationclick', (event) => {
|
| 183 |
+
console.log('[Service Worker] Notification clicked');
|
| 184 |
+
event.notification.close();
|
| 185 |
+
|
| 186 |
+
const urlToOpen = event.notification.data.url || '/';
|
| 187 |
+
|
| 188 |
+
event.waitUntil(
|
| 189 |
+
clients.matchAll({ type: 'window', includeUncontrolled: true })
|
| 190 |
+
.then((windowClients) => {
|
| 191 |
+
// Check if there's already a window open
|
| 192 |
+
for (let client of windowClients) {
|
| 193 |
+
if (client.url === urlToOpen && 'focus' in client) {
|
| 194 |
+
return client.focus();
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
// Open new window
|
| 198 |
+
if (clients.openWindow) {
|
| 199 |
+
return clients.openWindow(urlToOpen);
|
| 200 |
+
}
|
| 201 |
+
})
|
| 202 |
+
);
|
| 203 |
+
});
|
| 204 |
+
|
| 205 |
+
// Helper function for background sync
|
| 206 |
+
async function syncPendingAnalyses() {
|
| 207 |
+
// Future implementation: sync pending analyses to backend
|
| 208 |
+
console.log('[Service Worker] Syncing pending analyses...');
|
| 209 |
+
return Promise.resolve();
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
// Message handler for skip waiting
|
| 213 |
+
self.addEventListener('message', (event) => {
|
| 214 |
+
if (event.data && event.data.type === 'SKIP_WAITING') {
|
| 215 |
+
self.skipWaiting();
|
| 216 |
+
}
|
| 217 |
+
});
|
| 218 |
+
|
| 219 |
+
console.log('[Service Worker] Loaded successfully');
|
frontend/style.css
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/three_bg.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 3D Background with Three.js
|
| 2 |
+
// Theme: Dark space with "Nano Yellow" stars/particles
|
| 3 |
+
|
| 4 |
+
function initThreeBackground() {
|
| 5 |
+
const container = document.getElementById('canvas-container');
|
| 6 |
+
if (!container) return;
|
| 7 |
+
|
| 8 |
+
// PERFORMANCE OPTIMIZATION: Reduce count on mobile
|
| 9 |
+
const isMobile = window.innerWidth < 768;
|
| 10 |
+
const particleCount = isMobile ? 400 : 1200; // significantly fewer particles on mobile
|
| 11 |
+
|
| 12 |
+
// SCENE
|
| 13 |
+
const scene = new THREE.Scene();
|
| 14 |
+
scene.fog = new THREE.FogExp2(0x000000, 0.002);
|
| 15 |
+
|
| 16 |
+
// CAMERA
|
| 17 |
+
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1000);
|
| 18 |
+
camera.position.z = 500;
|
| 19 |
+
|
| 20 |
+
// RENDERER - PERFORMANCE TUNING
|
| 21 |
+
const renderer = new THREE.WebGLRenderer({
|
| 22 |
+
alpha: true,
|
| 23 |
+
antialias: !isMobile, // Disable antialias on mobile for performance
|
| 24 |
+
powerPreference: "high-performance" // Hint to browser
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
// Cap pixel ratio to 2 to avoid 9x rendering on 3x screens
|
| 28 |
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
| 29 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 30 |
+
renderer.setClearColor(0x000000, 0); // Transparent background
|
| 31 |
+
container.appendChild(renderer.domElement);
|
| 32 |
+
|
| 33 |
+
// Theme Check
|
| 34 |
+
const getThemeColors = () => {
|
| 35 |
+
const theme = localStorage.getItem('theme') || 'dark';
|
| 36 |
+
if (theme === 'light') {
|
| 37 |
+
return {
|
| 38 |
+
primary: 0x00AEEF, // Cyan
|
| 39 |
+
secondary: 0x0044CC, // Blue
|
| 40 |
+
gridPrimary: 0x00AEEF, // Cyan Grid
|
| 41 |
+
gridSecondary: 0xE0E0E0 // Light Grey Grid
|
| 42 |
+
};
|
| 43 |
+
}
|
| 44 |
+
return {
|
| 45 |
+
primary: 0xE3F514, // Nano Yellow
|
| 46 |
+
secondary: 0xFFFFFF, // White
|
| 47 |
+
gridPrimary: 0xE3F514, // Nano Yellow Grid
|
| 48 |
+
gridSecondary: 0x333333 // Dark Grey Grid
|
| 49 |
+
};
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
+
let themeColors = getThemeColors();
|
| 53 |
+
|
| 54 |
+
const geometry = new THREE.BufferGeometry();
|
| 55 |
+
const vertices = [];
|
| 56 |
+
const colors = [];
|
| 57 |
+
|
| 58 |
+
const color1 = new THREE.Color(themeColors.primary);
|
| 59 |
+
const color2 = new THREE.Color(themeColors.secondary);
|
| 60 |
+
|
| 61 |
+
for (let i = 0; i < particleCount; i++) {
|
| 62 |
+
// Random position
|
| 63 |
+
const x = (Math.random() - 0.5) * 2000;
|
| 64 |
+
const y = (Math.random() - 0.5) * 2000;
|
| 65 |
+
const z = (Math.random() - 0.5) * 2000;
|
| 66 |
+
vertices.push(x, y, z);
|
| 67 |
+
|
| 68 |
+
// Random color mix
|
| 69 |
+
const mixedColor = color1.clone().lerp(color2, Math.random() * 0.5);
|
| 70 |
+
colors.push(mixedColor.r, mixedColor.g, mixedColor.b);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
| 74 |
+
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
|
| 75 |
+
|
| 76 |
+
const material = new THREE.PointsMaterial({
|
| 77 |
+
size: 2,
|
| 78 |
+
vertexColors: true,
|
| 79 |
+
transparent: true,
|
| 80 |
+
opacity: 0.8,
|
| 81 |
+
sizeAttenuation: true
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
const particles = new THREE.Points(geometry, material);
|
| 85 |
+
scene.add(particles);
|
| 86 |
+
|
| 87 |
+
// GEOMETRIC SHAPES (Floating low-poly meshes)
|
| 88 |
+
const shapeGroup = new THREE.Group();
|
| 89 |
+
scene.add(shapeGroup);
|
| 90 |
+
|
| 91 |
+
function addFloatingShape(type, x, y, z, size) {
|
| 92 |
+
let geometry;
|
| 93 |
+
if (type === 'icosahedron') geometry = new THREE.IcosahedronGeometry(size, 0);
|
| 94 |
+
else if (type === 'octahedron') geometry = new THREE.OctahedronGeometry(size, 0);
|
| 95 |
+
|
| 96 |
+
const material = new THREE.MeshBasicMaterial({
|
| 97 |
+
color: themeColors.primary,
|
| 98 |
+
wireframe: true,
|
| 99 |
+
transparent: true,
|
| 100 |
+
opacity: 0.15
|
| 101 |
+
});
|
| 102 |
+
|
| 103 |
+
const mesh = new THREE.Mesh(geometry, material);
|
| 104 |
+
mesh.position.set(x, y, z);
|
| 105 |
+
shapeGroup.add(mesh);
|
| 106 |
+
return mesh;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Add a few floating shapes
|
| 110 |
+
const shapes = [];
|
| 111 |
+
shapes.push(addFloatingShape('icosahedron', -300, 100, -200, 60));
|
| 112 |
+
shapes.push(addFloatingShape('octahedron', 400, -150, -300, 80));
|
| 113 |
+
shapes.push(addFloatingShape('icosahedron', 0, 200, -400, 40));
|
| 114 |
+
|
| 115 |
+
// 3. INTERACTIVE 3D GRID FLOOR
|
| 116 |
+
const gridSize = 2000;
|
| 117 |
+
const gridDivisions = 40;
|
| 118 |
+
// Change const to let to allow reassignment
|
| 119 |
+
let gridHelper = new THREE.GridHelper(gridSize, gridDivisions, themeColors.gridPrimary, themeColors.gridSecondary);
|
| 120 |
+
gridHelper.position.y = -200; // Floor level
|
| 121 |
+
gridHelper.material.transparent = true;
|
| 122 |
+
gridHelper.material.opacity = 0.15;
|
| 123 |
+
scene.add(gridHelper);
|
| 124 |
+
|
| 125 |
+
// Watch for theme changes
|
| 126 |
+
// Store initial theme to avoid redundant updates on load
|
| 127 |
+
let currentTheme = localStorage.getItem('theme') || 'dark';
|
| 128 |
+
|
| 129 |
+
const observer = new MutationObserver((mutations) => {
|
| 130 |
+
mutations.forEach((mutation) => {
|
| 131 |
+
if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
|
| 132 |
+
const newTheme = document.documentElement.getAttribute('data-theme');
|
| 133 |
+
|
| 134 |
+
// Prevent infinite loop relative to initial set or same-value updates
|
| 135 |
+
if (newTheme === currentTheme) return;
|
| 136 |
+
currentTheme = newTheme;
|
| 137 |
+
|
| 138 |
+
const isLight = newTheme === 'light';
|
| 139 |
+
|
| 140 |
+
const newPrim = new THREE.Color(isLight ? 0x00AEEF : 0xE3F514);
|
| 141 |
+
const newSec = new THREE.Color(isLight ? 0x0044CC : 0xFFFFFF);
|
| 142 |
+
|
| 143 |
+
// Update Particles
|
| 144 |
+
const newColors = [];
|
| 145 |
+
for (let i = 0; i < particleCount; i++) {
|
| 146 |
+
const mixedColor = newPrim.clone().lerp(newSec, Math.random() * 0.5);
|
| 147 |
+
newColors.push(mixedColor.r, mixedColor.g, mixedColor.b);
|
| 148 |
+
}
|
| 149 |
+
particles.geometry.setAttribute('color', new THREE.Float32BufferAttribute(newColors, 3));
|
| 150 |
+
particles.geometry.attributes.color.needsUpdate = true;
|
| 151 |
+
|
| 152 |
+
// Update Shapes
|
| 153 |
+
shapes.forEach(shape => {
|
| 154 |
+
shape.material.color.set(newPrim);
|
| 155 |
+
});
|
| 156 |
+
|
| 157 |
+
// Update Grid
|
| 158 |
+
scene.remove(gridHelper);
|
| 159 |
+
// Create new grid using standard ThreeJS helper for fixed geometry colors
|
| 160 |
+
gridHelper = new THREE.GridHelper(gridSize, gridDivisions, isLight ? 0x00AEEF : 0xE3F514, isLight ? 0xE0E0E0 : 0x333333);
|
| 161 |
+
gridHelper.position.y = -200;
|
| 162 |
+
gridHelper.material.transparent = true;
|
| 163 |
+
gridHelper.material.opacity = 0.15;
|
| 164 |
+
scene.add(gridHelper);
|
| 165 |
+
}
|
| 166 |
+
});
|
| 167 |
+
});
|
| 168 |
+
|
| 169 |
+
observer.observe(document.documentElement, { attributes: true });
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
// MOUSE INTERACTION
|
| 173 |
+
let mouseX = 0;
|
| 174 |
+
let mouseY = 0;
|
| 175 |
+
let targetX = 0;
|
| 176 |
+
let targetY = 0;
|
| 177 |
+
|
| 178 |
+
const windowHalfX = window.innerWidth / 2;
|
| 179 |
+
const windowHalfY = window.innerHeight / 2;
|
| 180 |
+
|
| 181 |
+
document.addEventListener('mousemove', (event) => {
|
| 182 |
+
// Optimize: use requestAnimationFrame for mouse updates if needed, but direct is usually fine for coordinates
|
| 183 |
+
mouseX = (event.clientX - windowHalfX);
|
| 184 |
+
mouseY = (event.clientY - windowHalfY);
|
| 185 |
+
});
|
| 186 |
+
|
| 187 |
+
// RESIZE HANDLER (THROTTLED)
|
| 188 |
+
let resizeTimeout;
|
| 189 |
+
window.addEventListener('resize', () => {
|
| 190 |
+
if (!resizeTimeout) {
|
| 191 |
+
resizeTimeout = setTimeout(() => {
|
| 192 |
+
camera.aspect = window.innerWidth / window.innerHeight;
|
| 193 |
+
camera.updateProjectionMatrix();
|
| 194 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 195 |
+
|
| 196 |
+
// Update constraints for mouse calc
|
| 197 |
+
// windowHalfX = window.innerWidth / 2; // const variable can't be reassigned, generally okay not to update center exactly on resize for this effect
|
| 198 |
+
|
| 199 |
+
resizeTimeout = null;
|
| 200 |
+
}, 100);
|
| 201 |
+
}
|
| 202 |
+
});
|
| 203 |
+
|
| 204 |
+
// SCROLL INTERACTION
|
| 205 |
+
let scrollY = 0;
|
| 206 |
+
let targetScrollY = 0;
|
| 207 |
+
|
| 208 |
+
// Use passive listener for better scroll performance
|
| 209 |
+
document.addEventListener('scroll', () => {
|
| 210 |
+
scrollY = window.scrollY;
|
| 211 |
+
}, { passive: true });
|
| 212 |
+
|
| 213 |
+
// ANIMATION LOOP
|
| 214 |
+
function animate() {
|
| 215 |
+
requestAnimationFrame(animate);
|
| 216 |
+
|
| 217 |
+
// Smooth Scroll Interpolation
|
| 218 |
+
targetScrollY += (scrollY - targetScrollY) * 0.05;
|
| 219 |
+
|
| 220 |
+
// Mouse Parallax Calculation
|
| 221 |
+
targetX = mouseX * 0.001;
|
| 222 |
+
targetY = mouseY * 0.001;
|
| 223 |
+
|
| 224 |
+
// 1. Particle System Rotation
|
| 225 |
+
particles.rotation.y += 0.0005;
|
| 226 |
+
particles.rotation.x = targetScrollY * 0.0002;
|
| 227 |
+
|
| 228 |
+
// Mouse interaction for rotation
|
| 229 |
+
particles.rotation.y += 0.05 * (targetX - particles.rotation.y);
|
| 230 |
+
particles.rotation.x += 0.05 * (targetY - particles.rotation.x);
|
| 231 |
+
|
| 232 |
+
// 2. Camera Scroll Movement
|
| 233 |
+
const zoomFactor = targetScrollY * 0.1;
|
| 234 |
+
camera.position.z = 500 - zoomFactor; // Move closer
|
| 235 |
+
camera.position.y = -targetScrollY * 0.05; // Pan down subtly
|
| 236 |
+
|
| 237 |
+
// Stop zooming too close
|
| 238 |
+
if (camera.position.z < 100) camera.position.z = 100;
|
| 239 |
+
|
| 240 |
+
// 3. Floating Shapes Animation
|
| 241 |
+
for (let i = 0; i < shapes.length; i++) {
|
| 242 |
+
const shape = shapes[i];
|
| 243 |
+
shape.rotation.x += 0.002 * (i + 1);
|
| 244 |
+
shape.rotation.y += 0.002 * (i + 1);
|
| 245 |
+
shape.rotation.z = targetScrollY * 0.001 * (i % 2 === 0 ? 1 : -1);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
// 4. Grid Animation (Infinite Scroll)
|
| 249 |
+
// Move grid towards camera (z-axis)
|
| 250 |
+
// Using modulo based on local vars to avoid Date.now() overhead if high precision distinctness isn't needed
|
| 251 |
+
// But Date.now() is fine.
|
| 252 |
+
gridHelper.position.z = (Date.now() * 0.05) % (gridSize / gridDivisions);
|
| 253 |
+
gridHelper.position.z += targetScrollY * 0.5;
|
| 254 |
+
|
| 255 |
+
const cell = gridSize / gridDivisions;
|
| 256 |
+
if (gridHelper.position.z > cell) gridHelper.position.z -= cell;
|
| 257 |
+
|
| 258 |
+
renderer.render(scene, camera);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
animate();
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// Initialize when DOM is loaded
|
| 265 |
+
document.addEventListener('DOMContentLoaded', initThreeBackground);
|
frontend/vercel.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": 2,
|
| 3 |
+
"cleanUrls": true,
|
| 4 |
+
"trailingSlash": false
|
| 5 |
+
}
|
frontend/video_player.css
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Video Window Styles */
|
| 2 |
+
.video-window-container {
|
| 3 |
+
background: #1e1e1e;
|
| 4 |
+
border-radius: 12px;
|
| 5 |
+
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
| 6 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 7 |
+
overflow: hidden;
|
| 8 |
+
max-width: 1200px;
|
| 9 |
+
margin: 0 auto;
|
| 10 |
+
position: relative;
|
| 11 |
+
transform: translateZ(0);
|
| 12 |
+
/* Hardware accel */
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
.window-header {
|
| 16 |
+
background: #2d2d2d;
|
| 17 |
+
padding: 12px 16px;
|
| 18 |
+
display: flex;
|
| 19 |
+
align-items: center;
|
| 20 |
+
border-bottom: 1px solid rgba(0, 0, 0, 0.5);
|
| 21 |
+
position: relative;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.window-controls {
|
| 25 |
+
display: flex;
|
| 26 |
+
gap: 8px;
|
| 27 |
+
z-index: 2;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.control {
|
| 31 |
+
width: 12px;
|
| 32 |
+
height: 12px;
|
| 33 |
+
border-radius: 50%;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.control.red {
|
| 37 |
+
background: #ff5f56;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.control.yellow {
|
| 41 |
+
background: #ffbd2e;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.control.green {
|
| 45 |
+
background: #27c93f;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.window-title {
|
| 49 |
+
position: absolute;
|
| 50 |
+
width: 100%;
|
| 51 |
+
left: 0;
|
| 52 |
+
text-align: center;
|
| 53 |
+
color: #999;
|
| 54 |
+
font-size: 13px;
|
| 55 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 56 |
+
font-weight: 500;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.video-content-wrapper {
|
| 60 |
+
position: relative;
|
| 61 |
+
background: #000;
|
| 62 |
+
aspect-ratio: 16/9;
|
| 63 |
+
/* Enforce aspect ratio */
|
| 64 |
+
display: flex;
|
| 65 |
+
align-items: center;
|
| 66 |
+
justify-content: center;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.demo-video {
|
| 70 |
+
width: 100%;
|
| 71 |
+
height: 100%;
|
| 72 |
+
object-fit: cover;
|
| 73 |
+
/* Or contain, depending on video */
|
| 74 |
+
display: block;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.play-overlay {
|
| 78 |
+
position: absolute;
|
| 79 |
+
top: 0;
|
| 80 |
+
left: 0;
|
| 81 |
+
right: 0;
|
| 82 |
+
bottom: 0;
|
| 83 |
+
background: rgba(0, 0, 0, 0.4);
|
| 84 |
+
display: flex;
|
| 85 |
+
align-items: center;
|
| 86 |
+
justify-content: center;
|
| 87 |
+
cursor: pointer;
|
| 88 |
+
transition: opacity 0.3s ease;
|
| 89 |
+
z-index: 10;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.play-overlay.hidden {
|
| 93 |
+
opacity: 0;
|
| 94 |
+
pointer-events: none;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.play-button {
|
| 98 |
+
width: 80px;
|
| 99 |
+
height: 80px;
|
| 100 |
+
background: rgba(227, 245, 20, 0.9);
|
| 101 |
+
border-radius: 50%;
|
| 102 |
+
display: flex;
|
| 103 |
+
align-items: center;
|
| 104 |
+
justify-content: center;
|
| 105 |
+
color: #000;
|
| 106 |
+
font-size: 40px;
|
| 107 |
+
padding-left: 5px;
|
| 108 |
+
/* Visual center adjustment */
|
| 109 |
+
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 110 |
+
box-shadow: 0 0 30px rgba(227, 245, 20, 0.4);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.play-overlay:hover .play-button {
|
| 114 |
+
transform: scale(1.1);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.video-progress-bar {
|
| 118 |
+
position: absolute;
|
| 119 |
+
bottom: 0;
|
| 120 |
+
left: 0;
|
| 121 |
+
width: 100%;
|
| 122 |
+
height: 3px;
|
| 123 |
+
background: rgba(255, 255, 255, 0.1);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.progress-fill {
|
| 127 |
+
height: 100%;
|
| 128 |
+
width: 0%;
|
| 129 |
+
background: var(--accent-yellow);
|
| 130 |
+
transition: width 0.1s linear;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/* ==================== VIDEO PLAYER RESPONSIVE ==================== */
|
| 134 |
+
@media (max-width: 768px) {
|
| 135 |
+
.video-window-container {
|
| 136 |
+
border-radius: 16px;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.window-header {
|
| 140 |
+
padding: 10px 12px;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.play-button {
|
| 144 |
+
width: 60px;
|
| 145 |
+
height: 60px;
|
| 146 |
+
font-size: 28px;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.window-title {
|
| 150 |
+
font-size: 11px;
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
@media (max-width: 480px) {
|
| 155 |
+
.play-button {
|
| 156 |
+
width: 50px;
|
| 157 |
+
height: 50px;
|
| 158 |
+
font-size: 24px;
|
| 159 |
+
}
|
| 160 |
+
}
|