Files
NoN-Site/nonpacks/templates/library/project_detail.html
T
2026-08-05 17:31:15 -05:00

789 lines
41 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 &amp; 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 %}