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/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 @@ + + +
+ + +