789 lines
41 KiB
HTML
789 lines
41 KiB
HTML
{% extends 'base.html' %}
|
||
{% load static %}
|
||
{% load markdown %}
|
||
|
||
{% block title %}{{ project.title }} - Packs Site{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="project-header card">
|
||
<div class="project-avatar">
|
||
{% if project.thumbnail %}
|
||
<img src="{{ project.thumbnail_url }}" alt="{{ project.title }}">
|
||
{% else %}
|
||
<div class="default-avatar large">{{ project.title|first|upper }}</div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="project-header-main">
|
||
<div class="project-title-row">
|
||
<h1>{{ project.title }}</h1>
|
||
<span class="category-badge">{{ project.get_category_display }}</span>
|
||
{% if mods_manifest %}
|
||
{% if mods_manifest.minecraft %}<span class="category-badge modpack-badge" title="Minecraft version">{{ mods_manifest.minecraft }}</span>{% endif %}
|
||
{% for loader in mods_manifest.modloaders %}<span class="category-badge modpack-badge" title="Modloader">{{ loader }}</span>{% endfor %}
|
||
<span class="category-badge modpack-badge" title="Mod count">{{ mods_manifest.totals.total }} mods</span>
|
||
{% endif %}
|
||
{% if mod_manifest %}
|
||
{% for loader in mod_loaders %}<span class="category-badge modpack-badge" title="Modloader">{{ loader }}</span>{% endfor %}
|
||
{% if mod_count %}<span class="category-badge modpack-badge" title="Mod count">{{ mod_count }} mods</span>{% endif %}
|
||
{% endif %}
|
||
<span class="project-id-badge" title="Project ID">#{{ project.pk }}</span>
|
||
</div>
|
||
{% if project.summary %}<p class="project-summary">{{ project.summary }}</p>{% endif %}
|
||
<div class="project-meta">
|
||
<span class="project-owner"><i class="fas fa-user"></i>
|
||
<a href="{% url 'profiles:user_profile' project.owner.username %}">{{ project.owner.username }}</a>
|
||
</span>
|
||
<span class="project-stat"><i class="fas fa-download"></i> {{ project.downloads }}</span>
|
||
<span class="project-stat"><i class="fas fa-tag"></i> {{ versions|length }} version{{ versions|length|pluralize }}</span>
|
||
</div>
|
||
{% if can_edit %}
|
||
<div class="project-actions">
|
||
<a href="{% url 'library:version_upload' project.slug %}" class="btn btn-primary"><i class="fas fa-upload"></i> New version</a>
|
||
<a href="{% url 'library:asset_upload' project.slug %}" class="btn btn-secondary"><i class="fas fa-images"></i> Add media</a>
|
||
<a href="{% url 'library:project_edit' project.slug %}" class="btn btn-secondary"><i class="fas fa-pen"></i> Edit</a>
|
||
{% if request.user == project.owner %}
|
||
<a href="{% url 'library:contributors' project.slug %}" class="btn btn-secondary"><i class="fas fa-users"></i> Contributors</a>
|
||
<a href="{% url 'library:project_delete' project.slug %}" class="btn btn-danger"><i class="fas fa-trash"></i></a>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
{% if tags %}
|
||
<div class="project-tags card">
|
||
{% regroup tags by tag.category as tag_groups %}
|
||
{% for group in tag_groups %}
|
||
<div class="tag-group">
|
||
<span class="tag-group-label">
|
||
<span class="tag-dot" style="background: {{ group.grouper.color }};"></span>
|
||
{{ group.grouper.name }}
|
||
</span>
|
||
{% for link in group.list %}
|
||
<a href="{% url 'library:browse' %}?tag={{ link.tag.name }}" class="tag-chip"
|
||
style="border-color: {{ group.grouper.color }}; color: {{ group.grouper.color }};">{{ link.tag.name }}</a>
|
||
{% endfor %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="tab-bar" role="tablist">
|
||
<button class="tab-btn active" data-tab="{% if project.category == 'guide' %}guide{% else %}description{% endif %}" role="tab">
|
||
{% if project.category == 'guide' %}Guide{% else %}Description{% endif %}
|
||
</button>
|
||
<button class="tab-btn" data-tab="versions" role="tab">Versions</button>
|
||
{% if animation_manifest %}<button class="tab-btn" data-tab="animations" role="tab">Animations</button>{% endif %}
|
||
{% if models_manifest %}<button class="tab-btn" data-tab="models" role="tab">Models/Textures</button>{% endif %}
|
||
{% if logical_manifest %}<button class="tab-btn" data-tab="logical" role="tab">Logical</button>{% endif %}
|
||
{% if mods_manifest %}<button class="tab-btn" data-tab="mods" role="tab">Mods</button>{% endif %}
|
||
{% if plugins_manifest %}<button class="tab-btn" data-tab="plugins" role="tab">Plugins</button>{% endif %}
|
||
{% if skins_manifest %}<button class="tab-btn" data-tab="skins" role="tab">Skins</button>{% endif %}
|
||
{% if mod_manifest %}<button class="tab-btn" data-tab="modinfo" role="tab">Mod Info</button>{% endif %}
|
||
<button class="tab-btn" data-tab="gallery" role="tab">Gallery</button>
|
||
<button class="tab-btn" data-tab="rating" role="tab">Rating</button>
|
||
<button class="tab-btn" data-tab="forum" role="tab">Forum</button>
|
||
</div>
|
||
|
||
<section class="tab-panel active" id="tab-{% if project.category == 'guide' %}guide{% else %}description{% endif %}">
|
||
<div class="card">
|
||
{% if project.category == 'guide' %}
|
||
<div class="guide-docs">
|
||
{% if guide_docs %}
|
||
<div class="guide-switcher" role="tablist">
|
||
{% for doc in guide_docs %}
|
||
<button type="button" class="guide-switch-btn{% if forloop.first %} active{% endif %}"
|
||
data-url="{{ doc.url }}">{{ doc.filename }}</button>
|
||
{% endfor %}
|
||
</div>
|
||
<div class="markdown-body guide-content" id="guide-content">
|
||
<p class="empty-hint">Loading…</p>
|
||
</div>
|
||
{% else %}
|
||
<p class="empty-hint">No guide documents uploaded yet.</p>
|
||
{% endif %}
|
||
</div>
|
||
{% elif project.description %}
|
||
<div class="markdown-body project-description">{{ project.description|markdown }}</div>
|
||
{% else %}
|
||
<p class="empty-hint">No description yet.</p>
|
||
{% endif %}
|
||
</div>
|
||
</section>
|
||
|
||
<section class="tab-panel" id="tab-versions">
|
||
<div class="card">
|
||
<h2><i class="fas fa-history"></i> Versions</h2>
|
||
{% for version in versions %}
|
||
<div class="version-row">
|
||
<div class="version-info">
|
||
<div class="version-name-row">
|
||
<strong class="version-name">{{ version.version_name }}</strong>
|
||
{% if forloop.first %}<span class="latest-badge">latest</span>{% endif %}
|
||
{% if version.pack_format %}<span class="pack-format-badge">pack format {{ version.pack_format }}</span>{% endif %}
|
||
</div>
|
||
<span class="version-date"><i class="fas fa-clock"></i> {{ version.created_at|date:"M j, Y" }}</span>
|
||
<span class="version-downloads"><i class="fas fa-download"></i> {{ version.downloads }}</span>
|
||
</div>
|
||
{% if version.pack_description %}
|
||
<p class="pack-description"><i class="fas fa-info-circle"></i> {{ version.pack_description }}</p>
|
||
{% endif %}
|
||
{% if version.changelog %}
|
||
<div class="markdown-body version-changelog">{{ version.changelog|markdown }}</div>
|
||
{% endif %}
|
||
<div class="version-files">
|
||
{% for vf in version.files.all %}
|
||
<div class="version-file">
|
||
{% if vf.is_png %}
|
||
<img class="version-file-thumb" src="{% url 'library:file_request' vf.file.uuid %}" alt="{{ vf.filename }}">
|
||
{% elif vf.is_markdown %}
|
||
<i class="fas fa-file-lines version-file-icon"></i>
|
||
{% else %}
|
||
<i class="fas fa-file-archive version-file-icon"></i>
|
||
{% endif %}
|
||
<span class="version-file-name">{{ vf.filename }}</span>
|
||
<a class="btn btn-primary btn-sm" href="{% url 'library:version_file_download' project.slug version.pk vf.file.uuid %}" title="Download {{ vf.filename }}">
|
||
<i class="fas fa-download"></i>
|
||
</a>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% empty %}
|
||
<p class="empty-hint">No versions yet.</p>
|
||
{% endfor %}
|
||
</div>
|
||
</section>
|
||
|
||
{% if animation_manifest %}
|
||
<section class="tab-panel" id="tab-animations">
|
||
<div class="card">
|
||
<div class="animations-header">
|
||
<h2><i class="fas fa-film"></i> Animations</h2>
|
||
{% if animation_manifest.name %}
|
||
<p class="anim-pack-name">{{ animation_manifest.name }}
|
||
{% if animation_manifest.version %}<span class="pack-format-badge">v{{ animation_manifest.version }}</span>{% endif %}
|
||
</p>
|
||
{% endif %}
|
||
<p class="anim-authors">
|
||
<i class="fas fa-user-pen"></i> by
|
||
{% for author in animation_manifest.authors %}{{ author }}{% if not forloop.last %}, {% endif %}{% empty %}{{ project.owner.username }}{% endfor %}
|
||
</p>
|
||
{% if animation_manifest.description %}<p class="anim-description">{{ animation_manifest.description }}</p>{% endif %}
|
||
{% if not animation_manifest.animation_id %}
|
||
<p class="anim-warning"><i class="fas fa-exclamation-triangle"></i> This pack has no <code>animationframework.id</code> — it can't be reached through the update-check API.</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if anim_stats %}
|
||
<div class="animations-summary">
|
||
<span><strong>{{ anim_stats.total }}</strong> animations</span>
|
||
<span><strong>{{ anim_stats.player }}</strong> Player × Player</span>
|
||
<span><strong>{{ anim_stats.entity }}</strong> Entity × Player</span>
|
||
<span><strong>{{ anim_stats.solo }}</strong> solo</span>
|
||
{% if anim_stats.flagged %}<span class="anim-summary-flagged"><strong>{{ anim_stats.flagged }}</strong> flagged ⚠</span>{% endif %}
|
||
<span><strong>{{ anim_stats.tags|length }}</strong> tags</span>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="anim-grid">
|
||
{% for anim in animation_manifest.animations %}
|
||
<div class="anim-card">
|
||
<div class="anim-card-head">
|
||
<strong class="anim-name">{{ anim.name }}</strong>
|
||
<span class="anim-type-badge">{{ anim.type }}</span>
|
||
{% if anim.problem_tags %}
|
||
<span class="anim-problem" title="{{ anim.problem_tags|join:', ' }}"><i class="fas fa-exclamation-triangle"></i></span>
|
||
{% endif %}
|
||
</div>
|
||
<div class="anim-tags">
|
||
{% for tag in anim.content_tags %}
|
||
<span class="tag-chip" style="border-color: var(--md-sys-color-content, #d62828); color: var(--md-sys-color-content, #d62828);">{{ tag }}</span>
|
||
{% endfor %}
|
||
{% for tag in anim.animation_tags %}
|
||
<span class="tag-chip">{{ tag }}</span>
|
||
{% endfor %}
|
||
</div>
|
||
<div class="anim-actors">
|
||
{% for actor in anim.actors %}
|
||
<span class="actor-chip">
|
||
{% if actor.entity == 'player' %}
|
||
<i class="fas fa-user"></i> Player
|
||
{% else %}
|
||
<i class="fas fa-paw"></i> {{ actor.entity }}
|
||
{% endif %}
|
||
{% if actor.gender %}{% if actor.gender == 'male' %} ♂{% elif actor.gender == 'female' %} ♀{% endif %}{% endif %}
|
||
{% if actor.activity %}<small>· {{ actor.activity }}</small>{% endif %}
|
||
{% if actor.injector %}
|
||
<span class="injector-badge"{% if actor.injector != 'injector' %} title="Injector: {{ actor.injector_name }}"{% endif %}>
|
||
{% if actor.injector == 'injector' %}injector{% else %}{{ actor.injector_name }} ({{ actor.injector }}){% endif %}
|
||
</span>
|
||
{% endif %}
|
||
{% if actor.receiver %}<small>· receiver</small>{% endif %}
|
||
</span>
|
||
{% endfor %}
|
||
</div>
|
||
<div class="anim-stages">
|
||
<span class="stages-count">{{ anim.stages|length }} stage{{ anim.stages|length|pluralize }}</span>
|
||
{% for s in anim.stages %}
|
||
<span class="stage-chip"{% if s.climax %} title="climax stage"{% endif %}>
|
||
{% if s.climax %}<i class="fas fa-star"></i>{% endif %}{{ s.stage }}{% if s.loop %}↻{% endif %}{% if s.cycle_seconds %} {{ s.cycle_seconds }}s{% endif %}
|
||
</span>
|
||
{% endfor %}
|
||
</div>
|
||
{% if anim.weight or anim.block_requirements or anim.water %}
|
||
<div class="anim-extras">
|
||
{% if anim.weight %}<span class="anim-extra">weight {{ anim.weight }}</span>{% endif %}
|
||
{% if anim.block_requirements %}<span class="anim-extra"><i class="fas fa-cubes"></i> requires block support</span>{% endif %}
|
||
{% if anim.water %}<span class="anim-extra"><i class="fas fa-water"></i> water</span>{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
{% endif %}
|
||
|
||
{% if models_manifest %}
|
||
<section class="tab-panel" id="tab-models">
|
||
<div class="card">
|
||
<h2><i class="fas fa-cube"></i> Models & Textures</h2>
|
||
<div class="model-grid">
|
||
{% for model in models_manifest.models %}
|
||
<div class="model-card">
|
||
<div class="model-card-head">
|
||
<strong>{{ model.name }}</strong>
|
||
{% if model.ext == '.bbmodel' or model.ext == '.geo.json' or model.ext == '.jem' %}
|
||
<button type="button" class="btn btn-secondary btn-sm model-render-btn"
|
||
data-member="{{ model.member }}" data-name="{{ model.name }}"
|
||
data-model-format="{{ model.ext }}"
|
||
data-default-texture="{{ model.default_texture }}"
|
||
data-project-slug="{{ project.slug }}"
|
||
data-plugins="{{ autoload_plugins_json }}"
|
||
data-base-url="{% url 'library:pack_asset' project.slug versions.0.pk 'ASSET' %}"
|
||
data-vanilla-base-url="/static/vanilla/ASSET"><i class="fas fa-cube"></i> Render</button>
|
||
{% else %}
|
||
<span class="model-incompatible" title="glTF/GLB preview needs the desktop glTF Importer plugin, which the web viewer does not bundle">Incompatible</span>
|
||
{% endif %}
|
||
</div>
|
||
<p class="model-meta">
|
||
{% if model.ext %}<span class="model-format-badge">{{ model.ext }}</span>{% endif %}
|
||
{% if model.has_embedded_texture %}<span class="model-tex-badge embedded">textures embedded</span>{% endif %}
|
||
{{ model.bones }} bones · {{ model.cubes }} cubes
|
||
</p>
|
||
<div class="model-textures">
|
||
{% for member in model.textures %}
|
||
<img src="{% url 'library:pack_asset' project.slug versions.0.pk member %}" alt="{{ member }}" loading="lazy" title="{{ member }}">
|
||
{% empty %}
|
||
<span class="empty-hint">No textures</span>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% if models_manifest.textures %}
|
||
<h3 class="models-textures-title">Pack textures</h3>
|
||
<div class="model-textures strip">
|
||
{% for texture in models_manifest.textures %}
|
||
<img src="{% url 'library:pack_asset' project.slug versions.0.pk texture %}" alt="{{ texture }}" loading="lazy" title="{{ texture }}">
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</section>
|
||
{% endif %}
|
||
|
||
{% if logical_manifest %}
|
||
<section class="tab-panel" id="tab-logical">
|
||
<div class="card">
|
||
<h2><i class="fas fa-box"></i> What this pack adds</h2>
|
||
|
||
{% if logical_manifest.items %}
|
||
<h3>Items ({{ logical_manifest.items|length }})</h3>
|
||
<div class="logical-items">
|
||
{% for item in logical_manifest.items %}
|
||
<span class="logical-item">
|
||
{% if item.texture %}
|
||
<img src="{% url 'library:pack_asset' project.slug versions.0.pk item.texture %}" alt="" loading="lazy">
|
||
{% endif %}
|
||
{{ item.name }}
|
||
</span>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if logical_manifest.recipes %}
|
||
<h3>Recipes ({{ logical_manifest.recipes|length }})</h3>
|
||
<ul class="logical-list">
|
||
{% for r in logical_manifest.recipes %}
|
||
<li>
|
||
{% if r.disabled %}<span class="badge-disabled">disabled</span>{% endif %}
|
||
<code>{{ r.ingredients|join:' + ' }}</code> → <code>{{ r.result }}</code>
|
||
{% if r.type %}<small>({{ r.type }})</small>{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% endif %}
|
||
|
||
{% if logical_manifest.custom_recipes %}
|
||
<h3>Custom crafting</h3>
|
||
<ul class="logical-list">
|
||
{% for r in logical_manifest.custom_recipes %}
|
||
<li>{{ r.name }} → <code>{{ r.reward }}</code></li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% endif %}
|
||
|
||
{% if logical_manifest.advancements %}
|
||
<h3>Advancements</h3>
|
||
<ul class="logical-list">
|
||
{% for a in logical_manifest.advancements %}
|
||
<li>{{ a.name }}{% if a.reward %} → <code>{{ a.reward }}</code>{% endif %}</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% endif %}
|
||
|
||
{% if logical_manifest.functions %}
|
||
<h3>Functions ({{ logical_manifest.functions|length }}{% if logical_manifest.custom_functions %} + {{ logical_manifest.custom_functions }} custom{% endif %})</h3>
|
||
<div class="logical-tags">{% for f in logical_manifest.functions %}<code>{{ f }}</code>{% endfor %}</div>
|
||
{% endif %}
|
||
|
||
{% if logical_manifest.liquid_gains %}
|
||
<h3>Liquids</h3>
|
||
<div class="logical-tags">{% for l in logical_manifest.liquid_gains %}<code>{{ l }}</code>{% endfor %}</div>
|
||
{% endif %}
|
||
|
||
{% if logical_manifest.entity_profiles %}
|
||
<h3>Entity profiles</h3>
|
||
<div class="logical-tags">{% for e in logical_manifest.entity_profiles %}<code>{{ e }}</code>{% endfor %}</div>
|
||
{% endif %}
|
||
|
||
{% if logical_manifest.trinkets %}
|
||
<h3>Trinkets</h3>
|
||
<div class="logical-tags">{% for t in logical_manifest.trinkets %}<code>{{ t }}</code>{% endfor %}</div>
|
||
{% endif %}
|
||
</div>
|
||
</section>
|
||
{% endif %}
|
||
|
||
{% if mods_manifest %}
|
||
<section class="tab-panel" id="tab-mods">
|
||
<div class="card">
|
||
<h2><i class="fas fa-cubes"></i> Mods</h2>
|
||
{% if mods_manifest.files %}
|
||
<div class="mods-summary">
|
||
<span class="mods-source-badge">{{ mods_manifest.source }}</span>
|
||
{% if mods_manifest.minecraft %}<span class="mods-stat">MC {{ mods_manifest.minecraft }}</span>{% endif %}
|
||
{% if mods_manifest.modloaders %}<span class="mods-stat">{{ mods_manifest.modloaders|join:' / ' }}</span>{% endif %}
|
||
<span class="mods-stat">{{ mods_manifest.totals.total }} mods</span>
|
||
{% if mods_manifest.totals.required is not None %}
|
||
<span class="mods-stat">{{ mods_manifest.totals.required }} required</span>
|
||
<span class="mods-stat">{{ mods_manifest.totals.optional }} optional</span>
|
||
{% endif %}
|
||
</div>
|
||
<input type="search" class="mods-filter" id="mods-filter" placeholder="Filter mods…" aria-label="Filter mods">
|
||
<ul class="mods-list" id="mods-list">
|
||
{% for mod in mods_manifest.files %}
|
||
<li class="mods-item">
|
||
<i class="fas fa-cube"></i>
|
||
<span class="mods-name">{{ mod.name }}</span>
|
||
{% if mod.file_name and mod.file_name != mod.name %}<span class="mods-file">{{ mod.file_name }}</span>{% endif %}
|
||
{% if mod.env == 'client' %}<span class="mods-badge client">client</span>{% elif mod.env == 'server' %}<span class="mods-badge server">server</span>{% elif mod.env == 'optional' or mod.required is False %}<span class="mods-badge optional">optional</span>{% endif %}
|
||
{% if mod.url %}<a class="mods-link" href="{{ mod.url }}" target="_blank" rel="noopener noreferrer" title="Open project page"><i class="fas fa-external-link-alt"></i></a>{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% else %}
|
||
<p class="empty-hint">No mods detected in the uploaded pack.</p>
|
||
{% endif %}
|
||
</div>
|
||
</section>
|
||
{% endif %}
|
||
|
||
{% if plugins_manifest %}
|
||
<section class="tab-panel" id="tab-plugins">
|
||
<div class="card">
|
||
<h2><i class="fas fa-puzzle-piece"></i> Plugins</h2>
|
||
<div class="plugins-warning">
|
||
<i class="fas fa-exclamation-triangle"></i> <strong>Caution:</strong> this pack contains Blockbench plugins — executable JavaScript that runs in your browser. Plugins can crash Blockbench or run arbitrary code. Only open models from packs you trust.
|
||
</div>
|
||
<div class="plugins-toggle">
|
||
<label class="switch">
|
||
<input type="checkbox" id="plugins-hotload-toggle" checked>
|
||
<span class="slider"></span>
|
||
</label>
|
||
<span>Hotload this pack's plugins when opening a model</span>
|
||
</div>
|
||
<ul class="plugins-list">
|
||
{% for plugin in plugins_manifest.plugins %}
|
||
<li>
|
||
<i class="fas fa-file-code"></i>
|
||
<span class="plugins-name">{{ plugin.name }}</span>
|
||
{% if plugin.autoload %}<span class="plugins-badge autoload">autoload</span>{% endif %}
|
||
<span class="plugins-size">{{ plugin.size|filesizeformat }}</span>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
{% endif %}
|
||
|
||
{% if skins_manifest %}
|
||
<section class="tab-panel" id="tab-skins">
|
||
<div class="card">
|
||
<h2><i class="fas fa-user"></i> Skins</h2>
|
||
<div class="skin-grid">
|
||
{% for skin in skins_manifest.skins %}
|
||
<div class="skin-card">
|
||
<img class="skin-preview" src="{{ skin.url }}" alt="{{ skin.name }}" loading="lazy" title="{{ skin.name }}">
|
||
<div class="skin-card-head">
|
||
<strong>{{ skin.name }}</strong>
|
||
<span class="skin-size-badge">{{ skin.width }}x{{ skin.height }}</span>
|
||
</div>
|
||
<div class="skin-card-actions">
|
||
<label class="skin-model-label">
|
||
Model
|
||
<select class="skin-model-select">
|
||
<option value="steve">Steve (wide)</option>
|
||
<option value="alex">Alex (slim)</option>
|
||
</select>
|
||
</label>
|
||
<button type="button" class="btn btn-secondary btn-sm skin-render-btn"
|
||
data-url="{{ skin.url }}" data-name="{{ skin.name }}"><i class="fas fa-user"></i> Render</button>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
{% endif %}
|
||
|
||
{% if mod_manifest %}
|
||
<section class="tab-panel" id="tab-modinfo">
|
||
<div class="card">
|
||
<h2><i class="fas fa-cube"></i> Mod Info</h2>
|
||
{% if mod_manifest.no_mods %}
|
||
<div class="mods-warning"><i class="fas fa-exclamation-triangle"></i> No mods found at the root of the uploaded pack. Expected .jar files, or a .zip containing .jar files at its root (subfolders are ignored).</div>
|
||
{% endif %}
|
||
{% for mod in mod_manifest.mods %}
|
||
<div class="mod-card">
|
||
<div class="mod-card-head">
|
||
<strong>{{ mod.name }}</strong>
|
||
{% if mod.modloader %}<span class="mod-loader-badge">{{ mod.modloader }}</span>{% endif %}
|
||
{% if mod.version %}<span class="mod-version">{{ mod.version }}</span>{% endif %}
|
||
</div>
|
||
{% if mod.id %}<p class="mod-id">{{ mod.id }}</p>{% endif %}
|
||
{% if mod.description %}<p class="mod-desc">{{ mod.description }}</p>{% endif %}
|
||
<div class="mod-meta">
|
||
{% if mod.minecraft %}<span class="mod-stat">MC {{ mod.minecraft }}</span>{% endif %}
|
||
{% if mod.environment %}<span class="mod-stat">{{ mod.environment }}</span>{% endif %}
|
||
<span class="mod-stat">{{ mod.file_name }}</span>
|
||
</div>
|
||
{% if mod.dependencies %}
|
||
<ul class="mod-deps">
|
||
{% for dep in mod.dependencies %}
|
||
<li><i class="fas fa-link"></i> {{ dep.modId }}{% if dep.versionRange %} <code>{{ dep.versionRange }}</code>{% endif %} <span class="mod-dep-type">({{ dep.type }})</span></li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% endif %}
|
||
</div>
|
||
{% empty %}
|
||
{% if not mod_manifest.no_mods %}<p class="empty-hint">No mod metadata could be read from the uploaded files.</p>{% endif %}
|
||
{% endfor %}
|
||
</div>
|
||
</section>
|
||
{% endif %}
|
||
|
||
<section class="tab-panel" id="tab-gallery">
|
||
<div class="card">
|
||
<h2><i class="fas fa-images"></i> Gallery</h2> <div class="gallery-grid">
|
||
{% for asset in assets %}
|
||
<figure class="gallery-item" data-media-url="{{ asset.file_url }}" data-media-type="{% if asset.is_video %}video{% else %}image{% endif %}" data-caption="{{ asset.caption }}">
|
||
{% if asset.is_video or asset.is_gif %}
|
||
<img class="gallery-thumb" src="{% url 'library:asset_thumbnail' project.slug asset.pk %}" alt="{{ asset.caption }}" loading="lazy">
|
||
{% else %}
|
||
<img src="{{ asset.file_url }}" alt="{{ asset.caption }}" loading="lazy">
|
||
{% endif %}
|
||
<span class="gallery-expand" title="Open fullscreen"><i class="fas fa-expand"></i></span>
|
||
{% if asset.caption %}<figcaption>{{ asset.caption }}</figcaption>{% endif %}
|
||
{% if can_edit %}
|
||
<a href="{% url 'library:asset_delete' project.slug asset.pk %}" class="gallery-delete" title="Delete media"><i class="fas fa-trash"></i></a>
|
||
{% endif %}
|
||
</figure>
|
||
{% empty %}
|
||
<p class="empty-hint">No media yet.</p>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="tab-panel" id="tab-rating">
|
||
<div class="card">
|
||
<h2><i class="fas fa-thumbs-up"></i> Rating</h2>
|
||
{% if rating_stats.total %}
|
||
<div class="rating-summary">
|
||
<span class="rating-percent">{{ rating_stats.percent }}% positive</span>
|
||
<span class="rating-count">{{ rating_stats.total }} vote{{ rating_stats.total|pluralize }} · {{ rating_stats.positive }} up · {{ rating_stats.negative }} down</span>
|
||
<div class="rating-bar">
|
||
<div class="rating-bar-up" style="width: {{ rating_stats.percent }}%"></div>
|
||
<div class="rating-bar-down" style="width: {{ rating_stats.negative_percent }}%"></div>
|
||
</div>
|
||
</div>
|
||
{% else %}
|
||
<p class="empty-hint">No ratings yet — be the first to vote.</p>
|
||
{% endif %}
|
||
|
||
{% if can_comment %}
|
||
<div class="rating-vote">
|
||
<form method="post" action="{% url 'library:rate_project' project.slug %}">
|
||
{% csrf_token %}
|
||
<input type="hidden" name="value" value="1">
|
||
<button type="submit" class="rating-btn {% if user_rating %}active{% endif %}" title="Positive"><i class="fas fa-thumbs-up"></i> Good</button>
|
||
</form>
|
||
<form method="post" action="{% url 'library:rate_project' project.slug %}">
|
||
{% csrf_token %}
|
||
<input type="hidden" name="value" value="0">
|
||
<button type="submit" class="rating-btn {% if user_rating is False %}active{% endif %}" title="Negative"><i class="fas fa-thumbs-down"></i> Bad</button>
|
||
</form>
|
||
{% if user_rating is not None %}
|
||
<form method="post" action="{% url 'library:rate_project' project.slug %}">
|
||
{% csrf_token %}
|
||
<input type="hidden" name="value" value="">
|
||
<button type="submit" class="rating-btn" title="Clear your vote"><i class="fas fa-undo"></i></button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
{% else %}
|
||
<p class="empty-hint"><a href="{% url 'profiles:login' %}?next={{ request.path }}" class="btn btn-secondary btn-sm">Log in</a> to rate this pack.</p>
|
||
{% endif %}
|
||
</div>
|
||
</section>
|
||
|
||
<section class="tab-panel" id="tab-forum">
|
||
<div class="card">
|
||
<h2><i class="fas fa-comments"></i> Forum</h2>
|
||
|
||
{% if can_comment %}
|
||
<form method="post" action="{% url 'library:add_comment' project.slug %}" class="comment-form">
|
||
{% csrf_token %}
|
||
<textarea name="body" rows="4" placeholder="Write a comment… Markdown supported." required></textarea>
|
||
<button type="submit" class="btn btn-primary"><i class="fas fa-paper-plane"></i> Post</button>
|
||
</form>
|
||
{% else %}
|
||
<p class="empty-hint"><a href="{% url 'profiles:login' %}?next={{ request.path }}" class="btn btn-secondary btn-sm">Log in</a> to join the discussion.</p>
|
||
{% endif %}
|
||
|
||
<div class="comment-list">
|
||
{% for comment in comments %}
|
||
<div class="comment" id="comment-{{ comment.pk }}">
|
||
<div class="comment-head">
|
||
<strong>{{ comment.user.username }}</strong>
|
||
<span class="comment-time">{{ comment.created_at|timesince }} ago</span>
|
||
{% if comment.updated_at != comment.created_at %}<span class="comment-edited">· edited</span>{% endif %}
|
||
{% if can_comment and comment.user == request.user %}
|
||
<span class="comment-actions">
|
||
<a href="#" class="comment-edit btn btn-secondary btn-sm" data-comment="{{ comment.pk }}"><i class="fas fa-edit"></i> Edit</a>
|
||
<form method="post" action="{% url 'library:delete_comment' project.slug comment.pk %}" class="inline">
|
||
{% csrf_token %}
|
||
<button type="submit" class="link-btn" title="Delete"><i class="fas fa-trash"></i></button>
|
||
</form>
|
||
</span>
|
||
{% elif can_comment and request.user.is_staff %}
|
||
<span class="comment-actions">
|
||
<form method="post" action="{% url 'library:delete_comment' project.slug comment.pk %}" class="inline" title="Delete this comment">
|
||
{% csrf_token %}
|
||
<button type="submit" class="btn btn-secondary btn-sm"><i class="fas fa-trash"></i> Delete</button>
|
||
</form>
|
||
</span>
|
||
{% endif %}
|
||
</div>
|
||
<div class="comment-body markdown-body">{{ comment.body|markdown }}</div>
|
||
{% if can_comment and comment.user == request.user %}
|
||
<form method="post" action="{% url 'library:edit_comment' project.slug comment.pk %}" class="comment-edit-form" data-comment="{{ comment.pk }}" hidden>
|
||
{% csrf_token %}
|
||
<textarea name="body" rows="3" required>{{ comment.body }}</textarea>
|
||
<div class="comment-edit-actions">
|
||
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
||
<button type="button" class="btn btn-secondary btn-sm comment-edit-cancel">Cancel</button>
|
||
</div>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
{% empty %}
|
||
<p class="empty-hint">No comments yet.</p>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="modal-overlay" id="gallery-modal" hidden>
|
||
<div class="modal-content media-modal-content">
|
||
<button type="button" class="modal-close" data-close-modal aria-label="Close"><i class="fas fa-times"></i></button>
|
||
<div class="modal-media" id="gallery-modal-media"></div>
|
||
<p class="modal-caption" id="gallery-modal-caption"></p>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
{% if models_manifest or skins_manifest %}
|
||
<script src="{% static 'js/packs_preview.js' %}"></script>
|
||
{% endif %}
|
||
<script>
|
||
(function () {
|
||
const buttons = document.querySelectorAll('.tab-btn');
|
||
buttons.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
buttons.forEach(b => b.classList.remove('active'));
|
||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
|
||
});
|
||
});
|
||
|
||
// Land on the #rating / #forum tab when the URL points there.
|
||
const hashTab = location.hash.replace('#', '');
|
||
if (hashTab === 'rating' || hashTab === 'forum') {
|
||
const btn = document.querySelector(`.tab-btn[data-tab="${hashTab}"]`);
|
||
if (btn) btn.click();
|
||
}
|
||
|
||
// YouTube-style inline comment editing.
|
||
document.querySelectorAll('.comment-edit').forEach(link => {
|
||
link.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
const id = link.dataset.comment;
|
||
const body = document.querySelector(`#comment-${id} .comment-body`);
|
||
const form = document.querySelector(`#comment-${id} .comment-edit-form`);
|
||
if (body) body.hidden = true;
|
||
if (form) form.hidden = false;
|
||
});
|
||
});
|
||
document.querySelectorAll('.comment-edit-cancel').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const form = btn.closest('.comment-edit-form');
|
||
const id = form.dataset.comment;
|
||
const body = document.querySelector(`#comment-${id} .comment-body`);
|
||
if (body) body.hidden = false;
|
||
form.hidden = true;
|
||
});
|
||
});
|
||
|
||
// Gallery deletion without a page reload, behind a confirmation dialog.
|
||
document.querySelectorAll('.gallery-delete').forEach(link => {
|
||
link.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
if (!confirm('Delete this media? This cannot be undone.')) return;
|
||
const xhr = new XMLHttpRequest();
|
||
xhr.open('POST', link.getAttribute('href'));
|
||
xhr.setRequestHeader('X-CSRFToken', getCookie('csrftoken'));
|
||
xhr.onload = function () {
|
||
if (xhr.status === 200) {
|
||
const figure = link.closest('figure.gallery-item');
|
||
if (figure) figure.remove();
|
||
} else {
|
||
alert('Could not delete media.');
|
||
}
|
||
};
|
||
xhr.onerror = function () {
|
||
alert('Network error while deleting media.');
|
||
};
|
||
xhr.send();
|
||
});
|
||
});
|
||
|
||
// Guide document file-switcher (fetches + renders the selected .md).
|
||
const guideContent = document.getElementById('guide-content');
|
||
if (guideContent) {
|
||
const guideBtns = document.querySelectorAll('.guide-switch-btn');
|
||
function loadGuide(url) {
|
||
guideContent.innerHTML = '<p class="empty-hint">Loading…</p>';
|
||
fetch(url, { headers: { 'Accept': 'text/html' } })
|
||
.then(r => { if (!r.ok) throw new Error('http'); return r.text(); })
|
||
.then(html => { guideContent.innerHTML = html; })
|
||
.catch(() => { guideContent.innerHTML = '<p class="empty-hint">Could not load this document.</p>'; });
|
||
}
|
||
guideBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
guideBtns.forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
loadGuide(btn.dataset.url);
|
||
});
|
||
});
|
||
const first = guideBtns[0];
|
||
if (first) loadGuide(first.dataset.url);
|
||
}
|
||
|
||
// Full-screen gallery modal (images/gifs/videos; videos muted).
|
||
const galleryModal = document.getElementById('gallery-modal');
|
||
if (galleryModal) {
|
||
const mediaEl = document.getElementById('gallery-modal-media');
|
||
const captionEl = document.getElementById('gallery-modal-caption');
|
||
document.querySelectorAll('.gallery-item[data-media-url]').forEach(item => {
|
||
item.addEventListener('click', (e) => {
|
||
if (e.target.closest('.gallery-delete')) return;
|
||
const url = item.dataset.mediaUrl;
|
||
const type = item.dataset.mediaType;
|
||
mediaEl.innerHTML = type === 'video'
|
||
? '<video controls muted playsinline autoplay src="' + url + '"></video>'
|
||
: '<img src="' + url + '" alt="">';
|
||
captionEl.textContent = item.dataset.caption || '';
|
||
galleryModal.hidden = false;
|
||
});
|
||
});
|
||
}
|
||
|
||
// Close buttons for modals.
|
||
document.querySelectorAll('[data-close-modal]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const modal = btn.closest('.modal-overlay');
|
||
if (modal) {
|
||
modal.hidden = true;
|
||
const media = modal.querySelector('.modal-media');
|
||
if (media) media.innerHTML = '';
|
||
}
|
||
});
|
||
});
|
||
document.querySelectorAll('.modal-overlay').forEach(modal => {
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) modal.hidden = true;
|
||
});
|
||
});
|
||
|
||
// Model renderer (Blockbench showcase).
|
||
if (window.PacksPreview) {
|
||
document.querySelectorAll('.model-render-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => window.PacksPreview.open(btn));
|
||
});
|
||
document.querySelectorAll('.skin-render-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => window.PacksPreview.openSkin(btn));
|
||
});
|
||
}
|
||
|
||
// Live filter for the Mods tab.
|
||
const modsFilter = document.getElementById('mods-filter');
|
||
if (modsFilter) {
|
||
modsFilter.addEventListener('input', () => {
|
||
const q = modsFilter.value.toLowerCase();
|
||
document.querySelectorAll('#mods-list .mods-item').forEach(item => {
|
||
item.style.display = item.textContent.toLowerCase().includes(q) ? '' : 'none';
|
||
});
|
||
});
|
||
}
|
||
|
||
// Per-project "Don't hotload" switch for pack plugins.
|
||
const pluginsToggle = document.getElementById('plugins-hotload-toggle');
|
||
const pluginsSlug = document.querySelector('.model-render-btn')?.dataset.projectSlug;
|
||
if (pluginsToggle && pluginsSlug) {
|
||
const key = 'packs:no-hotload:' + pluginsSlug;
|
||
if (localStorage.getItem(key) === '1') pluginsToggle.checked = false;
|
||
pluginsToggle.addEventListener('change', () => {
|
||
if (pluginsToggle.checked) localStorage.removeItem(key);
|
||
else localStorage.setItem(key, '1');
|
||
});
|
||
}
|
||
})();
|
||
</script>
|
||
{% endblock %}
|