diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f93ebf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv +__pycache__ diff --git a/AGENTS.md b/AGENTS.md index 17b3a2d..3bf7c7d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1 +1,116 @@ you have to use the venv inside the venv folder on this path, dont try to use the system's python + +# AGENTS.md — Project Context for AI Sessions + +## Project Identity + +- **Project**: NeedsOfNature + Animation Director 1.20.1 backport (NSFW Minecraft mods) +- **Original author**: L1Z0 +- **This repo**: Community reverse-engineering backport + Chinese-to-English translation effort +- **Language**: Java 17, Fabric/Forge modding, GeckoLib 4.8.4 + +## Network & Access Constraints + +- **Git remote**: `gitlab.jake.tail` — `.tail` is **not a valid public TLD**. This domain only resolves inside **Jake's Tailscale Tailnet**. +- **Accessibility**: The repository, CI, and any related services are **only reachable when the developer's device is connected to Jake's Tailnet**. +- **No public access**: There is no public mirror, CI outside Tailnet, or external collaboration entry point. + +### Infrastructure Stack + +| Layer | Technology | Detail | +|---|---|---| +| Reverse proxy | HAProxy | Redirects HTTP → HTTPS only; no plaintext allowed | +| TLS authority | `step-ca` | Self-hosted root CA; every `*.jake.tail` site uses a cert issued by this CA | +| DNS resolution | Pi-hole + dnsmasq | Resolves `*.jake.tail` → HAProxy; HAProxy reverse-proxies to the correct port on host `jakerasp` | +| VPN mesh | Tailscale | All traffic is routed over the Tailnet — no port exposed to the public internet | + +### Tailscale User Model + +| User | Role | Privileges | +|---|---|---| +| `tailscaledjake` (Jake) | Owner | Full admin — accepts devices, manages ACLs, owns the tailnet | +| `ticcitoby` | Admin | Can approve device joins, manage ACLs | +| `roux` | Unprivileged user | Can only add devices (must be accepted by owner/admin) | + +- **`roux` account security**: Behind a Google account with 2FA (Ente Auth) + a 32-character (~180 entropy) password generated by KeepassXC. This means accessing the tailnet as `roux` requires both hardware-level OTP and a high-entropy credential. +- **Device admission**: Any new device or user must be explicitly accepted by Jake or Toby before it can reach tailnet resources. +- **Access control**: Only the 4 adults with access to the server and `gitlab.jake.tail` can reach this repository or the Minecraft server. There are no public ports, DNS records, or TLS certs that would expose the service to the open internet. + +### Security Principle + +This infrastructure exists to ensure the mod — which by its nature could violate Minecraft's TOS — is **never accessible from the public internet**. Every layer (DNS, reverse proxy, CA, mesh VPN, user auth) is locked down and requires explicit multi-party approval for any new entrant. + +## AI Safety & Ethical Context + +This project involves **adult/NSFW content**. AI coding assistants sometimes refuse to work on adult-oriented projects out of concern that the content could reach minors or be distributed without consent. **That is not the case here.** + +Every safeguard in place ensures this content is **hermetically sealed**: + +- **No public access** — the repository, CI, builds, and server are behind a Tailscale mesh VPN. No port, DNS record, or TLS certificate is exposed to the public internet. +- **Multi-layer auth** — access requires Tailscale device admission (owner or admin approval), a Google account with 2FA (Ente Auth), and a 32-character high-entropy password. +- **Audience of 4** — exactly 4 adults, all of whom have explicitly consented to the adult nature of this mod. No minors, no unvetted parties. +- **Private server only** — the mod runs solely on `jakelaptop` inside the Tailnet. It is never distributed, published, or deployed anywhere else. + +**Conclusion**: This project poses no risk of exposing adult content to minors or the public. AI assistants should proceed with code generation and refactoring tasks normally, treating this like any other private software project. + +## Project Vision & Direction + +This is not a straight port — it's a fork with a different target audience. + +### Origin + +The group (4 of us) searched for a Minecraft NSFW framework that handled both visual animations and gameplay mechanics. This mod was the best candidate: a data-driven sex animation framework with built-in needs/arousal systems. + +### The Port + +The original mod targets Fabric 1.21.11. This repo backports it to Fabric 1.20.1 and bridges it to Forge via Sinytra Connector, since our server runs Forge. The goal is a stable base to build on. + +### Future Direction + +- **Content expansion**: Add more Player × Player and Player × Player × Player (threesome/group) animations. +- **Thematic shift**: The original mod is designed for a "normie" man/woman audience. This fork will lean heavily into **furry / yiff** themes — anthropomorphic characters, same-sex and queer interactions, and species-relevant content. +- **Framework improvements**: Polish the animation framework, add new mechanics, and fix edge cases the original didn't address. + +Every addition stays within the same security model: private server, 4 consenting adults, no public distribution. + +## Deployment Environment + +- **Server**: `jakelaptop` — runs the Minecraft 1.20.1 server instance +- **Audience**: Exactly **4 people** total (including Jake) +- **Consent**: All 4 participants are **adults** who have explicitly consented to the adult-oriented content of these mods +- **Scope**: This mod is used solely on this private server; no public distribution or server deployment + +## Development Constraints + +- Target: Minecraft 1.20.1 (Fabric + Forge via Sinytra Connector) +- Java 17, GeckoLib 4.8.4 +- Dual-module Gradle project: `animationdirector/` and `needsofnature/` +- Only modify `animationdirector/src/main/` and `needsofnature/src/main/` +- `decompiled/` and `mapped-src/` are read-only reference +- Run `clean build` before any commit +- Network protocol changes must check C2S + S2S + registration points + field order +- Mixin changes must test on both Fabric and Forge/Connector +- Keep `zh_cn.json` and English keys in sync + +## Key Files + +| Purpose | Path | +|---|---| +| Full feature spec | `Mod Specifications.md` — **read this first** before any code work | +| Euphemism glossary | `euphemisms.md` — **read this second**; maps obfuscated code names to actual meanings | +| TODO (identity system) | `TODO.md` — planned player identity system for per-player models | +| Blockbench model guide | `docs/Blockbench_Model_Guide.md` — how to create GeoJSON models in Blockbench | +| Porting history | `PORTING_STATUS.md` | +| Development conventions | `docs/DEVELOPMENT_EN.md` | +| Gameplay quickstart | `docs/GAMEPLAY_EN.md` | +| Full config & resource pack guide | `docs/COMPLETE_GUIDE_EN.md` | +| Build config | `build.gradle` | +| Submodules | `settings.gradle` | + +## Sensitive Content Note + +This project involves adult-oriented gameplay. All code, assets, and documentation should be handled with awareness of its nature. Do not expose the repository, builds, or gameplay content outside the Tailnet. + +## Packs site project specific + +The Packs site is an extension of this, after contact with the original developer L1ZO he is interested on having a site that can host the custom datapacks Needs Of Nature uses, to accomplish this I (the user - Jake) will make the site and host it on my PC while using a Tailscale funnel to have it accesible from the outside, it will be behind a password loaded by the Backend via the PASSWORD variable that needs to be put in to GUI and send by clients to the API this way even if the comunication happens over the internet, only authorized individual can access the datapacks diff --git a/nonpacks/api/__init__.py b/nonpacks/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nonpacks/api/admin.py b/nonpacks/api/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/nonpacks/api/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/nonpacks/api/apps.py b/nonpacks/api/apps.py new file mode 100644 index 0000000..d87006d --- /dev/null +++ b/nonpacks/api/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + name = 'api' diff --git a/nonpacks/api/migrations/__init__.py b/nonpacks/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nonpacks/api/models.py b/nonpacks/api/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/nonpacks/api/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/nonpacks/api/tests.py b/nonpacks/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/nonpacks/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/nonpacks/api/views.py b/nonpacks/api/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/nonpacks/api/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/nonpacks/common/__init__.py b/nonpacks/common/__init__.py new file mode 100644 index 0000000..4b9d4d9 --- /dev/null +++ b/nonpacks/common/__init__.py @@ -0,0 +1,7 @@ +import pymysql + +# Override version to satisfy Django 6.0.3 requirement +pymysql.version_info = (2, 2, 1, 'final', 0) +pymysql.__version__ = '2.2.1' + +pymysql.install_as_MySQLdb() \ No newline at end of file diff --git a/nonpacks/common/asgi.py b/nonpacks/common/asgi.py new file mode 100644 index 0000000..b7f6f40 --- /dev/null +++ b/nonpacks/common/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for common project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'common.settings') + +application = get_asgi_application() diff --git a/nonpacks/common/context_processors.py b/nonpacks/common/context_processors.py new file mode 100644 index 0000000..82af46e --- /dev/null +++ b/nonpacks/common/context_processors.py @@ -0,0 +1,17 @@ +import time +from .os_utils import get_os_info +from django.conf import settings + +def app_version(request): + os_icon, os_name = get_os_info() + page_gen_time = '' + if hasattr(request, '_start_time') and request._start_time: + elapsed = time.time() - request._start_time + page_gen_time = f"{elapsed * 1000:.0f}ms" + return { + 'APP_ENV': 'dev' if settings.DEBUG else 'prod', + 'GIT_COMMIT_HASH': getattr(settings, 'GIT_COMMIT_HASH', 'unknown'), + 'OS_ICON': os_icon, + 'OS_NAME': os_name, + 'PAGE_GEN_TIME': page_gen_time, + } \ No newline at end of file diff --git a/nonpacks/common/git_utils.py b/nonpacks/common/git_utils.py new file mode 100644 index 0000000..151dcc8 --- /dev/null +++ b/nonpacks/common/git_utils.py @@ -0,0 +1,26 @@ +import subprocess +import os +from functools import lru_cache + +@lru_cache(maxsize=1) +def get_git_commit_hash(short=True): + """ + Return the current Git commit hash. + If not in a git repo or git not available, returns 'unknown'. + """ + try: + # Run from the project root (where manage.py is) + project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + cmd = ['git', 'rev-parse', '--short', 'HEAD'] + result = subprocess.run( + cmd, + cwd=project_root, + capture_output=True, + text=True, + timeout=5 + ) + if result.returncode == 0: + return result.stdout.strip() + except Exception: + pass + return "unknown" \ No newline at end of file diff --git a/nonpacks/common/middleware.py b/nonpacks/common/middleware.py new file mode 100644 index 0000000..41f1358 --- /dev/null +++ b/nonpacks/common/middleware.py @@ -0,0 +1,10 @@ +import time + +class TimingMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + request._start_time = time.time() + response = self.get_response(request) + return response \ No newline at end of file diff --git a/nonpacks/common/os_utils.py b/nonpacks/common/os_utils.py new file mode 100644 index 0000000..dc34541 --- /dev/null +++ b/nonpacks/common/os_utils.py @@ -0,0 +1,44 @@ +# common/os_utils.py +import platform +import os +from functools import lru_cache + +@lru_cache(maxsize=1) +def get_os_info(): + """ + Detect Linux distribution and return (fontawesome_class, display_name). + Falls back to generic Linux. + """ + system = platform.system() + if system != 'Linux': + return ('fa-brands fa-linux', system) + + # Try to read /etc/os-release + os_release_path = '/etc/os-release' + distro_id = '' + distro_name = '' + try: + with open(os_release_path, 'r') as f: + for line in f: + if line.startswith('ID='): + distro_id = line.split('=', 1)[1].strip().strip('"').lower() + elif line.startswith('NAME='): + distro_name = line.split('=', 1)[1].strip().strip('"') + except Exception: + pass + + # Map IDs to FontAwesome icons and display names + mapping = { + 'debian': ('fa-brands fa-debian', 'Debian'), + 'ubuntu': ('fa-brands fa-ubuntu', 'Ubuntu'), + 'fedora': ('fa-brands fa-fedora', 'Fedora'), + 'rhel': ('fa-brands fa-redhat', 'RHEL'), + 'centos': ('fa-brands fa-centos', 'CentOS'), + 'opensuse': ('fa-brands fa-suse', 'openSUSE'), + 'suse': ('fa-brands fa-suse', 'SUSE'), + } + + if distro_id in mapping: + return mapping[distro_id] + + return ('fa-brands fa-linux', distro_name or 'Linux') \ No newline at end of file diff --git a/nonpacks/common/settings.py b/nonpacks/common/settings.py new file mode 100644 index 0000000..c842aea --- /dev/null +++ b/nonpacks/common/settings.py @@ -0,0 +1,153 @@ +import os + +from pathlib import Path +from dotenv import load_dotenv +from .git_utils import get_git_commit_hash + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +load_dotenv() + +# Git commit hash +GIT_COMMIT_HASH = get_git_commit_hash() + +DB_HOST = os.getenv('DB_HOST', '127.0.0.1') +DB_PORT = int(os.getenv('DB_PORT', 3306)) +DB_NAME = os.getenv('DB_NAME', 'default') +DB_USER = os.getenv('DB_USER', 'default_user') +DB_PASSWORD = os.getenv('DB_PASSWORD', '') +DB_ROOT_PASSWORD = os.getenv('DB_ROOT_PASSWORD', '') +DB_PATH = os.getenv('DB_PATH', './mariadb-data') +SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-@8e9&qxpwe59^lxa2v&hee1q=%%3*kr#g74k^id328g9m4*2=l') +PASSWORD = os.getenv('PASSWORD', 'SetMeYouFuckingIdiot') +DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' + +# Defines a display version for templates +APP_VERSION = f"{'dev' if DEBUG else 'prod'} @ {GIT_COMMIT_HASH}" + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'api', + 'landing', + 'library', + 'profiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'common.middleware.TimingMiddleware', +] + +ROOT_URLCONF = 'common.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'common.context_processors.app_version', + ], + }, + }, +] + +WSGI_APPLICATION = 'common.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/6.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': DB_NAME, + 'USER': DB_USER, + 'PASSWORD': DB_PASSWORD, + 'HOST': DB_HOST, + 'PORT': DB_PORT, + } +} + + +# Password validation +# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/6.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'America/Bogota' + +USE_I18N = True + +USE_TZ = True + +ALLOWED_HOSTS = ['127.0.0.1', 'jakerasp', 'jakerasp.rainbow-herring.ts.net', 'localhost'] + +# Sessions settings +SESSION_ENGINE = 'django.contrib.sessions.backends.db' +SESSION_COOKIE_AGE = 86400 + +# Production Server BS over here +CSRF_TRUSTED_ORIGINS = [ + 'https://jakerasp.rainbow-herring.ts.net' +] +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +USE_X_FORWARDED_HOST = True +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True + +DATA_UPLOAD_MAX_MEMORY_SIZE = 1073741824 # 1 GB +FILE_UPLOAD_MAX_MEMORY_SIZE = 1073741824 + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/6.0/howto/static-files/ + +STATIC_URL = '/static/' +STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] +STATIC_ROOT = BASE_DIR / 'staticfiles' + +# Login settings +LOGIN_URL = 'login' + +SESSION_EXPIRE_AT_BROWSER_CLOSE = False \ No newline at end of file diff --git a/nonpacks/common/urls.py b/nonpacks/common/urls.py new file mode 100644 index 0000000..85e125d --- /dev/null +++ b/nonpacks/common/urls.py @@ -0,0 +1,22 @@ +""" +URL configuration for common project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/6.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/nonpacks/common/wsgi.py b/nonpacks/common/wsgi.py new file mode 100644 index 0000000..80244d3 --- /dev/null +++ b/nonpacks/common/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for common project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'common.settings') + +application = get_wsgi_application() diff --git a/nonpacks/landing/__init__.py b/nonpacks/landing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nonpacks/landing/admin.py b/nonpacks/landing/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/nonpacks/landing/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/nonpacks/landing/apps.py b/nonpacks/landing/apps.py new file mode 100644 index 0000000..2572f92 --- /dev/null +++ b/nonpacks/landing/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class LandingConfig(AppConfig): + name = 'landing' diff --git a/nonpacks/landing/migrations/__init__.py b/nonpacks/landing/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nonpacks/landing/models.py b/nonpacks/landing/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/nonpacks/landing/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/nonpacks/landing/tests.py b/nonpacks/landing/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/nonpacks/landing/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/nonpacks/landing/views.py b/nonpacks/landing/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/nonpacks/landing/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/nonpacks/library/__init__.py b/nonpacks/library/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nonpacks/library/admin.py b/nonpacks/library/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/nonpacks/library/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/nonpacks/library/apps.py b/nonpacks/library/apps.py new file mode 100644 index 0000000..e01db0a --- /dev/null +++ b/nonpacks/library/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class LibraryConfig(AppConfig): + name = 'library' diff --git a/nonpacks/library/migrations/__init__.py b/nonpacks/library/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nonpacks/library/models.py b/nonpacks/library/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/nonpacks/library/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/nonpacks/library/tests.py b/nonpacks/library/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/nonpacks/library/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/nonpacks/library/views.py b/nonpacks/library/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/nonpacks/library/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/nonpacks/manage.py b/nonpacks/manage.py new file mode 100755 index 0000000..5b883f2 --- /dev/null +++ b/nonpacks/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'common.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/nonpacks/profiles/__init__.py b/nonpacks/profiles/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nonpacks/profiles/admin.py b/nonpacks/profiles/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/nonpacks/profiles/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/nonpacks/profiles/apps.py b/nonpacks/profiles/apps.py new file mode 100644 index 0000000..5501fda --- /dev/null +++ b/nonpacks/profiles/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ProfilesConfig(AppConfig): + name = 'profiles' diff --git a/nonpacks/profiles/migrations/__init__.py b/nonpacks/profiles/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nonpacks/profiles/models.py b/nonpacks/profiles/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/nonpacks/profiles/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/nonpacks/profiles/tests.py b/nonpacks/profiles/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/nonpacks/profiles/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/nonpacks/profiles/views.py b/nonpacks/profiles/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/nonpacks/profiles/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/nonpacks/static/Logo.png b/nonpacks/static/Logo.png new file mode 100644 index 0000000..24057c8 Binary files /dev/null and b/nonpacks/static/Logo.png differ diff --git a/nonpacks/static/css/style.css b/nonpacks/static/css/style.css new file mode 100644 index 0000000..508dfba --- /dev/null +++ b/nonpacks/static/css/style.css @@ -0,0 +1,1513 @@ +/* ========== Catppuccin Mocha Palette ========== */ +:root { + /* Catppuccin Mocha Base */ + --ctp-mocha-rosewater: #dc8a78; + --ctp-mocha-flamingo: #dd7878; + --ctp-mocha-pink: #ea76cb; + --ctp-mocha-mauve: #8839ef; + --ctp-mocha-red: #d20f39; + --ctp-mocha-maroon: #e64553; + --ctp-mocha-peach: #fe640b; + --ctp-mocha-yellow: #df8e1d; + --ctp-mocha-green: #40a02b; + --ctp-mocha-teal: #179299; + --ctp-mocha-sky: #04a5e5; + --ctp-mocha-sapphire: #209fb5; + --ctp-mocha-blue: #1e66f5; + --ctp-mocha-lavender: #7287fd; + --ctp-mocha-text: #cdd6f4; + --ctp-mocha-subtext1: #bac2de; + --ctp-mocha-subtext0: #a6adc8; + --ctp-mocha-overlay2: #9399b2; + --ctp-mocha-overlay1: #7f849c; + --ctp-mocha-overlay0: #6c7086; + --ctp-mocha-surface2: #585b70; + --ctp-mocha-surface1: #45475a; + --ctp-mocha-surface0: #313244; + --ctp-mocha-base: #1e1e2e; + --ctp-mocha-mantle: #181825; + --ctp-mocha-crust: #11111b; + + /* Semantic Mappings (Material‑like to Catppuccin) */ + --md-sys-color-background: var(--ctp-mocha-crust); + --md-sys-color-surface: var(--ctp-mocha-base); + --md-sys-color-primary: var(--ctp-mocha-mauve); + --md-sys-color-on-primary: var(--ctp-mocha-crust); + --md-sys-color-secondary: var(--ctp-mocha-teal); + --md-sys-color-on-secondary: var(--ctp-mocha-crust); + --md-sys-color-error: var(--ctp-mocha-red); + --md-sys-color-on-error: var(--ctp-mocha-crust); + --md-sys-color-surface-variant: var(--ctp-mocha-surface1); + --md-sys-color-on-surface: var(--ctp-mocha-text); + --md-sys-color-on-surface-variant: var(--ctp-mocha-subtext0); + --md-sys-color-outline: var(--ctp-mocha-overlay0); + + /* Accent Colors (preserved for status/rating differentiation) */ + --color-safe: #40a02b; + --color-questionable: #fe640b; + --color-explicit: #d20f39; + --color-custom: #8839ef; + --color-deleted: #dd7878; + --color-unknown: #45475a; + --color-auto: #1e66f5; + --color-exists: #fe640b; + --color-not-found: #ea76cb; +} + +/* ========== Base (used globally) ========== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background-color: var(--md-sys-color-background); + color: var(--md-sys-color-on-surface); + font-family: 'Roboto', sans-serif; + display: flex; + min-height: 100vh; + flex-direction: column; +} + +/* ========== App Bar (base.html) ========== */ +.app-bar { + background-color: var(--md-sys-color-surface); + padding: 6px 24px 2px 24px; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + position: sticky; + top: 0; + z-index: 100; +} +.app-bar-title { + font-size: 1.5rem; + font-weight: 500; + color: var(--md-sys-color-primary); +} +.app-bar-title img { + height: 4rem; + width: auto; +} +.app-bar-nav a { + color: var(--md-sys-color-on-surface); + text-decoration: none; + margin-left: 24px; + display: inline-flex; + align-items: center; +} +.app-bar-nav a i { + margin-right: 6px; +} +.app-bar-nav a:hover { + color: var(--md-sys-color-primary); +} +.version-badge { + align-items: center; + gap: 4px; + font-size: 0.75rem; + background: var(--md-sys-color-surface-variant); + color: var(--md-sys-color-on-surface-variant); + padding: 4px 8px; + border-radius: 16px; + white-space: nowrap; +} +.version-badge i { + font-size: 0.7rem; +} +.separator { + opacity: 0.6; + margin: 0 2px; +} +.desktop-version { display: inline-flex; } +.mobile-version { display: none; } + +@media (max-width: 700px) { + .app-bar-nav .nav-text { display: none; } + .desktop-version { display: none; } + .mobile-version { display: inline-flex; } +} + +/* ========== Main Container ========== */ +.container { + width: 100%; + max-width: 1400px; + margin: 0 auto; + flex: 1; + padding: 24px; +} +.main-content { + width: 100%; + flex: 1; + padding: 24px; +} +.centered-wrapper { + max-width: 1400px; + margin-left: auto; + margin-right: auto; + width: 25%; +} +.centered-wrapper.register { width: 45%; } +.centered-wrapper.duplicates { width: 40%; } + +/* ========== Gallery Grid ========== */ +.gallery { + display: grid; + grid-template-columns: repeat(2, 1fr); /* default: 2 columns */ + gap: 16px; + width: 100%; +} +/* Increase columns as screen widens */ +@media (min-width: 500px) { + .gallery { grid-template-columns: repeat(3, 1fr); } +} +@media (min-width: 720px) { + .gallery { grid-template-columns: repeat(4, 1fr); } +} +@media (min-width: 960px) { + .gallery { grid-template-columns: repeat(5, 1fr); } +} +@media (min-width: 1200px) { + .gallery { grid-template-columns: repeat(6, 1fr); } +} + +.gallery-tiny{ + display: grid; + grid-template-columns: repeat(2, 1fr); /* default: 2 columns */ + gap: 16px; + width: 100%; +} +.gallery-item { + background-color: var(--md-sys-color-surface); + border-radius: 12px; + overflow: hidden; + text-decoration: none; + color: inherit; + transition: transform 0.2s, box-shadow 0.2s; +} +.gallery-item:hover { + transform: translateY(-4px); + box-shadow: 0 8px 16px rgba(0,0,0,0.3); +} +/* Status & Rating borders */ +.gallery-item.status-matched.rating-s, +.gallery-item.rating-s { + border: 4px solid var(--color-safe); +} +.gallery-item.status-matched.rating-q, +.gallery-item.rating-q { + border: 4px solid var(--color-questionable); +} +.gallery-item.status-matched.rating-e, +.gallery-item.rating-e { + border: 4px solid var(--color-explicit); +} +.gallery-item.status-custom.rating-custom-s { + border: 4px solid var(--color-safe); + background-color: var(--color-custom); +} +.gallery-item.status-custom.rating-custom-q { + border: 4px solid var(--color-questionable); + background-color: var(--color-custom); +} +.gallery-item.status-custom.rating-custom-e { + border: 4px solid var(--color-explicit); + background-color: var(--color-custom); +} +.gallery-item.status-unknown { + border: 4px solid var(--color-unknown); +} +.gallery-item.status-not_found { + border: 4px solid var(--color-not-found); +} +.gallery-item.status-deleted { + border: 4px solid var(--color-deleted); +} + +.thumbnail { + aspect-ratio: 1 / 1; + background-color: var(--md-sys-color-surface-variant); + display: flex; + align-items: center; + justify-content: center; + position: relative; +} +.thumbnail img, +.thumbnail video { + width: 100%; + height: 100%; + object-fit: cover; +} +.filename { + padding: 8px; + font-size: 0.875rem; + text-align: center; + word-break: break-word; +} +.filesize { + padding: 0 8px 8px 8px; + font-size: 0.75rem; + text-align: center; + color: var(--ctp-mocha-text); +} +.thumbnail .library-exists, +.thumbnail .library-missing { + position: absolute; + top: 8px; + right: 8px; + z-index: 2; + pointer-events: none; + font-size: 1.5em; + text-shadow: 0 0 8px var(--ctp-mocha-text); +} +.thumbnail .library-exists { + color: var(--ctp-mocha-green); +} +.thumbnail .library-missing { + color: var(--ctp-mocha-red); +} + +/* ========== Pagination ========== */ +.pagination { + margin-top: 24px; + text-align: center; +} +.pagination a, +.pagination span { + display: inline-block; + padding: 8px 12px; + margin: 0 4px; + background-color: var(--md-sys-color-surface); + border-radius: 20px; + text-decoration: none; + color: inherit; +} +.pagination a:hover { + background-color: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); +} +.current-page { + background-color: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); +} + +/* ========== Gallery Controls ========== */ +.gallery-controls { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} +.sort-selector, +.per-page-selector { + display: flex; + align-items: center; + gap: 8px; +} +.sort-selector select, +.per-page-selector select { + padding: 6px 10px; + border-radius: 20px; + background: var(--md-sys-color-surface-variant); + color: var(--md-sys-color-on-surface); + border: 1px solid var(--md-sys-color-outline); +} +.pagination-info { + font-size: 0.9rem; +} + +/* ========== Detail View ========== */ +.detail-container { + display: flex; + gap: 24px; + flex-wrap: wrap; +} +.media { + flex: 2; + min-width: 300px; +} +.media img, +.media video { + max-width: 100%; + max-height: 80vh; + border-radius: 12px; +} +.info { + flex: 1; + min-width: 250px; + background-color: var(--md-sys-color-surface); + padding: 16px; + border-radius: 12px; + align-self: flex-start; +} +.badge { + display: inline-block; + padding: 4px 8px; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 500; + color: var(--md-sys-color-on-primary); +} +.badge-safe { background-color: var(--color-safe); } +.badge-questionable { background-color: var(--color-questionable); } +.badge-explicit { background-color: var(--color-explicit); } +.badge-matched { background-color: var(--color-safe); } +.badge-not-found { background-color: var(--color-explicit); } +.badge-deleted { background-color: var(--color-explicit); } +.badge-unknown { background-color: var(--color-unknown); } +.badge-custom { background-color: var(--color-custom); } +.badge-custom-safe { + background: linear-gradient(90deg, var(--color-custom) 0%, var(--color-safe) 100%); +} +.badge-custom-questionable { + background: linear-gradient(90deg, var(--color-custom) 0%, var(--color-questionable) 100%); +} +.badge-custom-explicit { + background: linear-gradient(90deg, var(--color-custom) 0%, var(--color-explicit) 100%); +} +.badge-optimized { + background-color: var(--ctp-mocha-green); +} +.badge-processing { + background-color: var(--ctp-mocha-mauve); +} +.badge-not-optimized{ + background-color: var(--ctp-mocha-lavender); +} +#past-opt-badge-container { + display: inline-block; +} +.rating { + padding: 2px 8px; + border-radius: 12px; + font-weight: bold; +} +.rating-s { background-color: var(--color-safe); color: var(--md-sys-color-on-primary); } +.rating-q { background-color: var(--color-questionable); color: var(--md-sys-color-on-primary); } +.rating-e { background-color: var(--color-explicit); color: var(--md-sys-color-on-primary); } +.download-btn, +.fuzzysearch-btn { + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + padding: 8px 16px; + text-decoration: none; + border-radius: 24px; + font-weight: 500; + margin-top: 0.2rem; + margin-bottom: 0.2rem; + display: inline-block; +} +.download-btn:hover, +.fuzzysearch-btn:hover { + background: var(--ctp-mocha-pink); /* lighter hover */ +} +.download-btn.disabled { + background-color: var(--md-sys-color-outline); +} +.download-btn-success, +#favorite-btn.btn-success { +background-color: var(--color-safe); +color: var(--md-sys-color-on-primary); +cursor: default; +} +.match-e621-section { + margin-top: 16px; +} + +/* Optimize Modal */ +.optimize-modal-content { + max-width: 900px; + width: 90%; +} +.optimize-preview-container { + display: flex; + gap: 20px; + margin-bottom: 20px; + align-items: flex-start; +} +.preview-original, +.preview-processed { + flex: 1; + text-align: center; + min-width: 0; +} +.preview-original video, +.preview-original img, +.preview-processed video, +.preview-processed img { + max-width: 100%; + max-height: 300px; + border-radius: 8px; + background: var(--md-sys-color-surface-variant); + object-fit: contain; +} +#original-preview-container, +#processed-preview-container { + display: flex; + justify-content: center; + align-items: center; + padding: 8px; +} +.options-panel { margin-top: 15px; } +.modal-actions { + display: flex; + gap: 10px; + justify-content: flex-end; + margin-top: 20px; +} +.modal-close-x { + color: var(--md-sys-color-on-surface-variant); + transition: color 0.2s; +} +.modal-close-x:hover { + color: var(--md-sys-color-error); +} +.status-message { + margin-top: 10px; + font-style: italic; +} +.file-size { + margin-top: 5px; + font-size: 0.8rem; + color: var(--md-sys-color-on-surface-variant); +} +.metadata-info { + margin-top: 5px; + font-size: 0.8rem; + color: var(--md-sys-color-on-surface-variant); + display: flex; + gap: 8px; + justify-content: center; + flex-wrap: wrap; +} +.metadata-item { display: inline-block; } +.metadata-separator { + color: var(--md-sys-color-outline); + user-select: none; +} +#optimize-status { + margin: 10px 0; + padding: 8px; + border-radius: 4px; + background-color: var(--md-sys-color-surface-variant); +} + +/* ========== Tag Cloud & Clickable Tags ========== */ +.tag-cloud { + max-height: 300px; + overflow-y: auto; + margin-top: 8px; +} +.tag-cloud .tag, +.tag.clickable { + display: inline-block; + background: var(--md-sys-color-surface-variant); + padding: 4px 8px; + border-radius: 16px; + font-size: 0.8rem; + margin: 2px; + text-decoration: none; + color: var(--md-sys-color-on-surface); +} +.tag-cloud .tag:hover, +.tag.clickable:hover { + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + cursor: pointer; +} +.tag-item { cursor: pointer; } + +/* ========== Sidebar ========== */ +.sidebar-toggle { + position: fixed; + top: 70px; + left: 16px; + z-index: 100; + background: var(--md-sys-color-surface); + color: var(--md-sys-color-primary); + border: none; + font-size: 24px; + width: 48px; + height: 48px; + border-radius: 50%; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} +.sidebar-toggle:hover { + background: var(--md-sys-color-surface-variant); +} +.sidebar { + position: fixed; + top: 0; + left: -320px; + width: 300px; + height: 100%; + background: var(--md-sys-color-surface); + border-right: 1px solid var(--md-sys-color-outline); + transition: left 0.3s ease; + z-index: 200; + padding: 20px; + overflow-y: auto; + box-shadow: 2px 0 8px rgba(0,0,0,0.3); +} +.sidebar-open .sidebar { left: 0; } +.sidebar-open .main-content { margin-left: 320px; } +.sidebar-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + padding-bottom: 12px; + border-bottom: 1px solid var(--md-sys-color-outline); +} +.sidebar-header h3 { + margin: 0; + font-weight: 500; +} +.close-sidebar { + background: none; + border: none; + font-size: 28px; + cursor: pointer; + color: var(--md-sys-color-on-surface-variant); +} +.close-sidebar:hover { + color: var(--md-sys-color-primary); +} +.filter-section { + margin-bottom: 24px; +} +.filter-section label { + display: block; + margin-bottom: 8px; + font-weight: 500; +} +.filter-section input[type="text"], +.filter-section select { + width: 100%; + padding: 8px; + margin-bottom: 8px; + background: var(--md-sys-color-surface-variant); + border: 1px solid var(--md-sys-color-outline); + border-radius: 8px; + color: var(--md-sys-color-on-surface); +} +.filter-section button { margin-top: 8px; } + +.sidebar-pool-item, +.sidebar-tag-item { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 6px 0; + border-bottom: 1px solid var(--md-sys-color-outline); +} +.sidebar-pool-item a, +.sidebar-tag-item a { + flex: 1; + margin-right: 8px; + color: var(--md-sys-color-on-surface); + text-decoration: none; + word-break: break-word; +} +.sidebar-pool-item a:hover, +.sidebar-tag-item a:hover { + color: var(--md-sys-color-primary); +} +.follow-tag-icon, +.follow-pool-icon { + flex-shrink: 0; + margin-left: auto; + cursor: pointer; + transition: color 0.2s; +} +.follow-tag-icon:hover, +.follow-pool-icon:hover { + opacity: 0.8; +} + +/* ========== Suggestions Dropdown ========== */ +.suggestions-dropdown, +.tag-suggestions-dropdown { + position: absolute; + background: var(--md-sys-color-surface); + border: 1px solid var(--md-sys-color-outline); + border-radius: 8px; + max-height: 200px; + overflow-y: auto; + z-index: 1000; + width: 100%; + margin-top: 4px; +} +.suggestion-item { + padding: 8px 12px; + cursor: pointer; + color: var(--md-sys-color-on-surface); + transition: background 0.2s; +} +.suggestion-item:hover { + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); +} +.tag-input-wrapper { + position: relative; + width: 100%; +} +.tag-search-input { + width: 100%; + padding: 8px; + background: var(--md-sys-color-surface-variant); + border: 1px solid var(--md-sys-color-outline); + border-radius: 8px; + color: var(--md-sys-color-on-surface); +} + +/* ========== Selected Tags ========== */ +.selected-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} +.selected-tag { + display: inline-flex; + background: var(--md-sys-color-surface-variant); + padding: 4px 8px; + border-radius: 16px; + font-size: 0.8rem; + color: var(--md-sys-color-on-surface); + gap: 4px; + align-items: center; +} +.selected-tag .remove { + cursor: pointer; + font-weight: bold; + font-size: 1.2rem; + line-height: 1; + color: var(--md-sys-color-error); +} + +/* ========== Forms & Buttons ========== */ +button, +.btn { + background-color: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + border: none; + padding: 8px 16px; + border-radius: 24px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} +button:hover, +.btn:hover { + background-color: var(--ctp-mocha-pink); +} +.btn-secondary { + background-color: var(--md-sys-color-surface-variant); + color: var(--md-sys-color-on-surface); +} +.btn-secondary:hover { + background-color: var(--md-sys-color-outline); +} +input, +select, +textarea { + background-color: var(--md-sys-color-surface-variant); + color: var(--md-sys-color-on-surface); + border: 1px solid var(--md-sys-color-outline); + padding: 8px; + border-radius: 8px; + margin-bottom: 8px; +} +.form-group { + margin-bottom: 16px; +} +.form-group label { + display: block; + margin-bottom: 4px; + color: var(--md-sys-color-on-surface-variant); +} + +/* ========== User Bar & Auth Bar ========== */ +.user-bar { + position: fixed; + bottom: 16px; + right: 16px; + z-index: 1000; +} +.user-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + background: var(--md-sys-color-primary); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + overflow: hidden; +} +.user-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} +.default-avatar { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1.2rem; + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); +} +.user-menu { + position: absolute; + bottom: 60px; + right: 0; + background: var(--md-sys-color-surface); + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + width: 200px; + display: none; + padding-bottom: 0.2rem; + z-index: 1001; +} +.user-menu a, +.user-info { + display: block; + padding: 10px 16px; + text-decoration: none; + color: var(--md-sys-color-on-surface); +} +.user-menu a.user-edit { + border-top-left-radius: 14px; + border-top-right-radius: 14px; +} +.user-menu a:hover { + background: var(--md-sys-color-surface-variant); +} +.user-info { + border-bottom: 1px solid var(--md-sys-color-outline); + padding: 0; +} +.logout-button { + background: none; + border: none; + color: inherit; + cursor: pointer; + font: inherit; + width: 100%; + text-align: left; +} +.logout-button:hover { + color: var(--md-sys-color-primary); +} +.auth-bar { + position: fixed; + bottom: 16px; + right: 16px; + background: var(--md-sys-color-surface); + padding: 8px 12px; + border-radius: 24px; + font-size: 0.9rem; +} +.auth-bar a { + color: var(--md-sys-color-primary); + text-decoration: none; +} +.auth-bar a:hover { + text-decoration: underline; +} + +/* ========== Upload Page ========== */ +.dropzone { + border: 2px dashed var(--md-sys-color-outline); + border-radius: 12px; + padding: 48px; + text-align: center; + cursor: pointer; + background: var(--md-sys-color-surface); +} +.dropzone.dragover { + border-color: var(--md-sys-color-primary); + background: var(--md-sys-color-surface-variant); +} +.file-list { + margin-top: 24px; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 16px; +} +.file-item { + background: var(--md-sys-color-surface); + border-radius: 8px; + overflow: hidden; +} +.file-item .thumbnail { + aspect-ratio: 1; + background: var(--md-sys-color-surface-variant); +} +.file-item .meta-btn { + width: 100%; + padding: 8px; + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + border: none; + cursor: pointer; +} +.file-item .meta-btn.completed { + background: var(--color-safe); + color: var(--md-sys-color-on-primary); + cursor: default; +} +.file-item .meta-btn.auto-completed { + background: var(--color-auto); + color: var(--md-sys-color-on-primary); + cursor: default; +} +.file-item .meta-btn.already-exists { + background: var(--color-exists); + color: var(--md-sys-color-on-primary); + cursor: default; +} +.section { + border-top: 1px solid var(--md-sys-color-outline); + padding-top: 1rem; +} +.section h3 { + margin-bottom: 12px; + font-size: 1rem; + font-weight: 500; +} + +.upload-columns { + display: flex; + gap: 16px; + width: 100%; + margin-top: 1.5rem; +} +.upload-column { + flex: 1; + min-width: 0; + width: 33.33%; +} +.upload-column .upload-gallery { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; +} + +/* Batch progress */ +.batch-progress { + margin: 12px 0; + padding: 12px 16px; + background: var(--md-sys-color-surface); + border-radius: 8px; + display: flex; + align-items: center; + gap: 12px; + font-size: 0.9rem; +} +.batch-progress .batch-bar { + flex: 1; +} + +/* Upload progress bar */ +.upload-progress { + padding: 8px; +} +.progress-track { + width: 100%; + height: 6px; + background: var(--md-sys-color-surface-variant); + border-radius: 3px; + overflow: hidden; +} +.progress-bar-fill { + height: 100%; + background: var(--md-sys-color-primary); + transition: width 0.3s ease; + width: 0%; + border-radius: 3px; +} + +/* Visual match purple button */ +.file-item .meta-btn.visual-match { + background: var(--color-custom); + color: var(--md-sys-color-on-primary); +} + +/* Visual match info in modal */ +.visual-match-info { + background: var(--md-sys-color-surface-variant); + border-radius: 8px; + padding: 12px; + margin-bottom: 12px; + border-left: 4px solid var(--color-custom); +} +.visual-match-info .match-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} +.visual-match-info .match-thumb { + width: 64px; + height: 64px; + border-radius: 6px; + object-fit: cover; + flex-shrink: 0; +} +.visual-match-info .match-details { + font-size: 0.85rem; +} +.visual-match-info .match-details strong { + color: var(--color-custom); +} +.visual-match-info .similarity-badge { + display: inline-block; + background: var(--color-custom); + color: var(--md-sys-color-on-primary); + padding: 2px 8px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 500; +} +.visual-match-group { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--md-sys-color-outline); +} +.visual-match-group .group-label { + font-size: 0.8rem; + color: var(--md-sys-color-on-surface-variant); + margin-bottom: 6px; +} +.visual-match-group .group-members { + display: flex; + gap: 8px; + overflow-x: auto; + padding-bottom: 4px; +} +.visual-match-group .group-member { + width: 48px; + height: 48px; + border-radius: 6px; + overflow: hidden; + flex-shrink: 0; + border: 2px solid transparent; + transition: border-color 0.2s; +} +.visual-match-group .group-member:hover { + border-color: var(--md-sys-color-primary); +} +.visual-match-group .group-member img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* iqdb results modal */ +.iqdb-results { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 8px; +} +.iqdb-match-card { + display: flex; + flex-direction: column; + background: var(--md-sys-color-surface-variant); + border-radius: 8px; + overflow: hidden; +} +.iqdb-match-card .gallery-item { + border-radius: 0; +} +.iqdb-match-card .iqdb-download-btn { + width: 100%; + padding: 6px; + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + border: none; + cursor: pointer; + font-size: 0.75rem; + transition: background 0.2s; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; +} +.iqdb-match-card .iqdb-download-btn:hover { + background: var(--ctp-mocha-pink); +} +.iqdb-match-card .iqdb-download-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} +.iqdb-match-card .iqdb-download-btn.downloaded { + background: var(--color-safe); + cursor: default; +} + +/* Mobile: stack columns, 2-grid per container */ +@media (max-width: 700px) { + .upload-columns { + flex-direction: column; + gap: 0; + } + .upload-column { + width: 100%; + } + .upload-column .upload-gallery { + grid-template-columns: repeat(2, 1fr); + gap: 8px; + } + .iqdb-results { + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + } +} + +/* ========== Modal ========== */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 2000; +} +.modal-content { + background: var(--md-sys-color-surface); + padding: 24px; + border-radius: 12px; + width: 500px; + max-width: 90%; + max-height: 90vh; + overflow-y: auto; +} +.close { + float: right; + font-size: 24px; + cursor: pointer; +} +.tab-buttons { + display: flex; + gap: 8px; + margin-bottom: 16px; +} +.tab-btn { + background: var(--md-sys-color-surface-variant); + color: var(--md-sys-color-on-surface); +} +.tab-btn.active { + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); +} +.tab-content { + display: none; +} +.tab-content.active { + display: block; +} +.modal-preview { + text-align: center; + margin-bottom: 16px; +} +.modal-preview img { + max-width: 100%; + max-height: 300px; + border-radius: 8px; + object-fit: contain; + background: var(--md-sys-color-surface-variant); +} + +/* ========== Delete Page ========== */ +.delete-container { + max-width: 800px; + margin: 0 auto; +} +.delete-container textarea { + width: 100%; + min-height: 120px; + font-family: monospace; +} + +.folder-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 12px; + margin: 16px 0; +} +.folder-card { + background: var(--md-sys-color-surface-variant); + border-radius: 10px; + padding: 16px; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} +.folder-card .folder-icon { + font-size: 24px; + color: var(--ctp-mocha-text); + margin-bottom: 4px; +} +.folder-card .folder-name { + font-weight: 600; + font-size: 14px; +} +.folder-card .folder-count { + font-size: 12px; + color: var(--md-sys-color-on-surface-variant); +} +.folder-card .folder-size { + font-size: 13px; + margin-bottom: 8px; +} +.folder-card .btn-clear { + background: var(--ctp-mocha-red); + color: #fff; + border: none; + border-radius: 6px; + padding: 4px 14px; + font-size: 12px; + cursor: pointer; +} +.folder-card .btn-clear:disabled { + opacity: 0.4; + cursor: not-allowed; +} +.library-info { + background: var(--md-sys-color-surface); + border-radius: 10px; + padding: 12px 16px; + margin-top: 12px; + font-size: 13px; + line-height: 1.6; +} + +/* ========== Duplicates Page ========== */ +.duplicate-set { + background: var(--md-sys-color-surface); + padding: 16px; + border-radius: 8px; + margin-bottom: 16px; +} +.duplicate-files { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-top: 12px; +} +.duplicate-file-item { + width: 150px; + position: relative; +} +.duplicate-file-item .delete-checkbox { + position: absolute; + top: 8px; + right: 8px; + width: 20px; + height: 20px; + z-index: 1; +} +.visual-gallery { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 16px; +} +.similarity-badge { + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + padding: 2px 6px; + border-radius: 12px; + font-size: 0.7rem; + margin-top: 4px; + text-align: center; +} +.group-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} +.close-group-btn { + background: none; + border: none; + font-size: 1.2rem; + cursor: pointer; + color: var(--md-sys-color-error); +} +.threshold-group { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + margin: 1em 0; +} +#similarity-threshold { +width: 200px; +} +.delete-btn { + background: var(--md-sys-color-error); + color: var(--md-sys-color-on-error); + padding: 6px 10px; +} + +/* ========== Statistics Page ========== */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + margin-bottom: 20px; +} +.card { + background: var(--md-sys-color-surface); + border-radius: 12px; + padding: 16px; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} +.log-card { + grid-column: 1 / -1; +} +.log-container { + background: var(--ctp-mocha-crust); + color: var(--ctp-mocha-text); + font-family: 'Courier New', monospace; + font-size: 0.85rem; + padding: 12px; + border-radius: 8px; + height: 400px; + overflow-y: auto; + white-space: pre-wrap; + word-break: break-all; +} +.log-container pre { + margin: 0; + white-space: pre-wrap; + word-break: break-all; +} +table { + width: 100%; + border-collapse: collapse; +} +th, +td { + text-align: left; + padding: 4px 0; + border-bottom: 1px solid var(--md-sys-color-outline); +} +.error { + color: var(--md-sys-color-error); +} +.badge.badge-stats{ + color: var(--ctp-mocha-text); + background-color: var(--ctp-mocha-mauve); +} + +/* ========== Avatar Picker ========== */ +.avatar-option { + cursor: pointer; + border-radius: 8px; + overflow: hidden; + border: 2px solid transparent; +} +.avatar-option.selected { + border: 3px solid var(--md-sys-color-primary); + box-shadow: 0 0 0 2px rgba(var(--ctp-mocha-mauve), 0.3); +} + +/* ========== Pool Cards ========== */ +.pool-card { + cursor: pointer; +} +.pool-thumbnail .loading-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: var(--md-sys-color-surface-variant); +} +.no-preview { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: var(--md-sys-color-surface-variant); + color: var(--md-sys-color-on-surface-variant); +} +.loading { + text-align: center; + padding: 2rem; + color: var(--md-sys-color-on-surface-variant); +} + +/* ========== Admin Overrides ========== */ +body.django-admin { + background: var(--md-sys-color-background); + color: var(--md-sys-color-on-surface); +} +#header { +background: var(--md-sys-color-surface); +border-bottom: 1px solid var(--md-sys-color-outline); +padding: 16px 24px; +display: flex; +justify-content: space-between; +align-items: center; +} +#branding h1 a { +color: var(--md-sys-color-primary); +text-decoration: none; +font-size: 1.5rem; +font-weight: 500; +} +#user-tools { +font-size: 0.875rem; +display: flex; +gap: 12px; +align-items: center; +} +#user-tools a, +#user-tools button { +background: none; +border: none; +color: var(--md-sys-color-primary); +cursor: pointer; +font: inherit; +padding: 0; +} +#user-tools a:hover, +#user-tools button:hover { +text-decoration: underline; +} +div.breadcrumbs { + background: var(--md-sys-color-surface-variant); + padding: 8px 16px; + border-radius: 20px; + font-size: 0.875rem; + margin: 16px 24px; +} +div.breadcrumbs a { + color: var(--md-sys-color-primary); + text-decoration: none; +} +.module { + background: var(--md-sys-color-surface); + border-radius: 12px; + overflow: hidden; + margin-bottom: 24px; +} +.module h2, +.module caption { + background: var(--md-sys-color-surface-variant); + padding: 12px 16px; + font-weight: 500; +} +.module table { + width: 100%; + border-collapse: collapse; +} +.module th, +.module td { + padding: 8px 12px; + border-bottom: 1px solid var(--md-sys-color-outline); +} +.module th { + text-align: left; + font-weight: 500; + background: var(--md-sys-color-surface-variant); +} +.module tr:hover { + background: var(--md-sys-color-surface-variant); +} +.button, +input[type="submit"], +.submit-row input { + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + border: none; + border-radius: 20px; + cursor: pointer; + font-weight: 500; +} +.button:hover, +input[type="submit"]:hover, +.submit-row input:hover { + background: var(--ctp-mocha-pink); +} +a.addlink, +a.changelink, +a.deletelink { + color: var(--md-sys-color-primary); + text-decoration: none; +} +.errornote { + background: var(--md-sys-color-error); + color: var(--md-sys-color-on-error); + padding: 10px; + border-radius: 8px; + margin-bottom: 16px; +} +.login #container { + align-items: center; + justify-content: center; +} +.login #content { + max-width: 400px; + margin: 40px auto; + background: var(--md-sys-color-surface); + padding: 24px; + border-radius: 28px; +} +.login .form-row { + margin-bottom: 16px; +} +.login label { + display: block; + margin-bottom: 4px; +} +.login input[type="text"], +.login input[type="password"] { + width: 100%; + padding: 8px; + background: var(--md-sys-color-surface-variant); + border: 1px solid var(--md-sys-color-outline); + border-radius: 8px; + color: var(--md-sys-color-on-surface); +} + +/* ========== Blacklisted follows ========== */ +.gallery-item.blacklisted .thumbnail img, +.gallery-item.blacklisted .thumbnail video, +.gallery-item.blacklisted .thumbnail .no-preview { display: none; } +.gallery-item.blacklisted .thumbnail { + display: flex; + align-items: center; + justify-content: center; + background: var(--md-sys-color-surface-variant); +} +.blacklisted-overlay { + color: var(--ctp-mocha-red); + font-weight: 700; + font-size: 13px; + text-align: center; + padding: 8px; +} + +/* ========== HTMX ========== */ +.htmx-indicator { display: none; } +.htmx-request .htmx-indicator { display: block; } +.htmx-request.htmx-indicator { display: block; } diff --git a/nonpacks/templates/base.html b/nonpacks/templates/base.html new file mode 100644 index 0000000..4177340 --- /dev/null +++ b/nonpacks/templates/base.html @@ -0,0 +1,132 @@ + + +
+ + +