working Mods Cat
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-08-05 15:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('library', '0010_version_skins_manifest'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='version',
|
||||||
|
name='mod_manifest',
|
||||||
|
field=models.JSONField(blank=True, default=dict),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -211,6 +211,7 @@ class Version(models.Model):
|
|||||||
mods_manifest = models.JSONField(default=dict, blank=True)
|
mods_manifest = models.JSONField(default=dict, blank=True)
|
||||||
plugins_manifest = models.JSONField(default=dict, blank=True)
|
plugins_manifest = models.JSONField(default=dict, blank=True)
|
||||||
skins_manifest = models.JSONField(default=dict, blank=True)
|
skins_manifest = models.JSONField(default=dict, blank=True)
|
||||||
|
mod_manifest = models.JSONField(default=dict, blank=True)
|
||||||
animation_id = models.CharField(max_length=128, blank=True, default='', db_index=True)
|
animation_id = models.CharField(max_length=128, blank=True, default='', db_index=True)
|
||||||
animation_manifest = models.JSONField(default=dict, blank=True)
|
animation_manifest = models.JSONField(default=dict, blank=True)
|
||||||
models_manifest = models.JSONField(default=dict, blank=True)
|
models_manifest = models.JSONField(default=dict, blank=True)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ from .zips import (
|
|||||||
read_pack_mcmeta,
|
read_pack_mcmeta,
|
||||||
read_plugins_manifest,
|
read_plugins_manifest,
|
||||||
read_skins_manifest,
|
read_skins_manifest,
|
||||||
|
read_mod_version_manifest,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -335,6 +336,7 @@ def project_detail(request, slug):
|
|||||||
mods_manifest = None
|
mods_manifest = None
|
||||||
plugins_manifest = None
|
plugins_manifest = None
|
||||||
skins_manifest = None
|
skins_manifest = None
|
||||||
|
mod_manifest = None
|
||||||
anim_stats = None
|
anim_stats = None
|
||||||
latest = versions.first()
|
latest = versions.first()
|
||||||
if project.category == 'guide' and latest is not None:
|
if project.category == 'guide' and latest is not None:
|
||||||
@@ -346,6 +348,7 @@ def project_detail(request, slug):
|
|||||||
mods_manifest = latest.mods_manifest or None
|
mods_manifest = latest.mods_manifest or None
|
||||||
plugins_manifest = latest.plugins_manifest or None
|
plugins_manifest = latest.plugins_manifest or None
|
||||||
skins_manifest = latest.skins_manifest or None
|
skins_manifest = latest.skins_manifest or None
|
||||||
|
mod_manifest = latest.mod_manifest or None
|
||||||
if animation_manifest:
|
if animation_manifest:
|
||||||
anims = animation_manifest.get('animations') or []
|
anims = animation_manifest.get('animations') or []
|
||||||
entity_count = sum(1 for a in anims if a.get('type') == 'Entity x Player')
|
entity_count = sum(1 for a in anims if a.get('type') == 'Entity x Player')
|
||||||
@@ -359,6 +362,16 @@ def project_detail(request, slug):
|
|||||||
'tags': sorted({t for a in anims for t in (a.get('content_tags') or [])}),
|
'tags': sorted({t for a in anims for t in (a.get('content_tags') or [])}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod_loaders = []
|
||||||
|
mod_count = 0
|
||||||
|
if mod_manifest:
|
||||||
|
mods = mod_manifest.get('mods') or []
|
||||||
|
mod_count = len(mods)
|
||||||
|
for m in mods:
|
||||||
|
loader = m.get('modloader')
|
||||||
|
if loader and loader not in mod_loaders:
|
||||||
|
mod_loaders.append(loader)
|
||||||
|
|
||||||
autoload_plugins_json = ''
|
autoload_plugins_json = ''
|
||||||
if plugins_manifest:
|
if plugins_manifest:
|
||||||
members = [
|
members = [
|
||||||
@@ -380,6 +393,9 @@ def project_detail(request, slug):
|
|||||||
'mods_manifest': mods_manifest,
|
'mods_manifest': mods_manifest,
|
||||||
'plugins_manifest': plugins_manifest,
|
'plugins_manifest': plugins_manifest,
|
||||||
'skins_manifest': skins_manifest,
|
'skins_manifest': skins_manifest,
|
||||||
|
'mod_manifest': mod_manifest,
|
||||||
|
'mod_loaders': mod_loaders,
|
||||||
|
'mod_count': mod_count,
|
||||||
'autoload_plugins_json': autoload_plugins_json,
|
'autoload_plugins_json': autoload_plugins_json,
|
||||||
'anim_stats': anim_stats,
|
'anim_stats': anim_stats,
|
||||||
}
|
}
|
||||||
@@ -657,6 +673,15 @@ def _reparse_version(version):
|
|||||||
version.logical_manifest = read_logical_manifest(path) or {}
|
version.logical_manifest = read_logical_manifest(path) or {}
|
||||||
version.plugins_manifest = read_plugins_manifest(path) or {} if version.project.category == 'model' else {}
|
version.plugins_manifest = read_plugins_manifest(path) or {} if version.project.category == 'model' else {}
|
||||||
version.skins_manifest = read_skins_manifest(path) or {} if version.project.category == 'skin' else {}
|
version.skins_manifest = read_skins_manifest(path) or {} if version.project.category == 'skin' else {}
|
||||||
|
if version.project.category == 'mod':
|
||||||
|
entries = [
|
||||||
|
(vf.filename, _stored_path(vf.file))
|
||||||
|
for vf in version.files.select_related('file').all()
|
||||||
|
if vf.filename.lower().endswith(('.jar', '.zip'))
|
||||||
|
]
|
||||||
|
version.mod_manifest = read_mod_version_manifest(entries)
|
||||||
|
else:
|
||||||
|
version.mod_manifest = {}
|
||||||
manifest = parse_mods_manifest(path, release.file.original_filename)
|
manifest = parse_mods_manifest(path, release.file.original_filename)
|
||||||
version.mods_manifest = manifest if manifest.get('files') else {}
|
version.mods_manifest = manifest if manifest.get('files') else {}
|
||||||
fmt, desc = read_pack_mcmeta(path)
|
fmt, desc = read_pack_mcmeta(path)
|
||||||
@@ -680,6 +705,7 @@ def _finalize_version(project, version_name, changelog, temp_uploads, actor):
|
|||||||
mods_manifest = {}
|
mods_manifest = {}
|
||||||
plugins_manifest = None
|
plugins_manifest = None
|
||||||
skins_manifest = None
|
skins_manifest = None
|
||||||
|
mod_file_entries = []
|
||||||
animation_id = ''
|
animation_id = ''
|
||||||
animation_manifest = None
|
animation_manifest = None
|
||||||
models_manifest = None
|
models_manifest = None
|
||||||
@@ -696,7 +722,12 @@ def _finalize_version(project, version_name, changelog, temp_uploads, actor):
|
|||||||
temp.status = 'used'
|
temp.status = 'used'
|
||||||
temp.save(update_fields=['status'])
|
temp.save(update_fields=['status'])
|
||||||
|
|
||||||
is_archive = (index.original_filename or '').lower().endswith(('.zip', '.mrpack'))
|
if project.category == 'mod':
|
||||||
|
mod_name = (index.original_filename or '').lower()
|
||||||
|
if mod_name.endswith(('.jar', '.zip')):
|
||||||
|
mod_file_entries.append((index.original_filename, _stored_path(index)))
|
||||||
|
|
||||||
|
is_archive = (index.original_filename or '').lower().endswith(('.zip', '.mrpack', '.jar'))
|
||||||
if not is_archive:
|
if not is_archive:
|
||||||
continue
|
continue
|
||||||
path = _stored_path(index)
|
path = _stored_path(index)
|
||||||
@@ -741,6 +772,9 @@ def _finalize_version(project, version_name, changelog, temp_uploads, actor):
|
|||||||
if skins_manifest is not None:
|
if skins_manifest is not None:
|
||||||
version.skins_manifest = skins_manifest
|
version.skins_manifest = skins_manifest
|
||||||
update_fields.append('skins_manifest')
|
update_fields.append('skins_manifest')
|
||||||
|
if project.category == 'mod' and mod_file_entries:
|
||||||
|
version.mod_manifest = read_mod_version_manifest(mod_file_entries)
|
||||||
|
update_fields.append('mod_manifest')
|
||||||
if animation_id:
|
if animation_id:
|
||||||
version.animation_id = animation_id
|
version.animation_id = animation_id
|
||||||
update_fields.append('animation_id')
|
update_fields.append('animation_id')
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ never re-reads large archives. All functions take the on-disk path of an
|
|||||||
adopted file (under MEDIA_ROOT) and return plain dicts/values.
|
adopted file (under MEDIA_ROOT) and return plain dicts/values.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import struct
|
import struct
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
@@ -59,6 +61,170 @@ def read_skins_manifest(path):
|
|||||||
zf.close()
|
zf.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _open_zip_any(source):
|
||||||
|
"""Open a zip from a path or raw bytes."""
|
||||||
|
if isinstance(source, (bytes, bytearray)):
|
||||||
|
return zipfile.ZipFile(io.BytesIO(source))
|
||||||
|
return _open_zip(source)
|
||||||
|
|
||||||
|
|
||||||
|
def _mc_from_filename(filename):
|
||||||
|
"""Best-effort MC version from a mod filename like 'x-1.0+mc26.2.jar'."""
|
||||||
|
m = re.search(r'mc(\d+(?:\.\d+)*)', str(filename).lower())
|
||||||
|
return m.group(1) if m else ''
|
||||||
|
|
||||||
|
|
||||||
|
def _manifest_value(raw, key):
|
||||||
|
for line in raw.splitlines():
|
||||||
|
if line.startswith(key + ':'):
|
||||||
|
return line.split(':', 1)[1].strip()
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def read_mod_manifest(source):
|
||||||
|
"""Parse a single Minecraft mod jar (path or bytes) into metadata or None.
|
||||||
|
|
||||||
|
Loader detection order: fabric.mod.json → quilt.mod.json →
|
||||||
|
META-INF/neoforge.mods.toml → META-INF/mods.toml → META-INF/MANIFEST.MF.
|
||||||
|
"""
|
||||||
|
zf = _open_zip_any(source)
|
||||||
|
if zf is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
names = set(zf.namelist())
|
||||||
|
if 'fabric.mod.json' in names:
|
||||||
|
data = _decode(zf.read('fabric.mod.json'))
|
||||||
|
depends = data.get('depends') or {}
|
||||||
|
deps = [
|
||||||
|
{'modId': k, 'type': 'required', 'versionRange': v}
|
||||||
|
for k, v in depends.items() if k and v
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
'id': data.get('id') or '',
|
||||||
|
'name': data.get('name') or data.get('id') or '',
|
||||||
|
'version': data.get('version') or '',
|
||||||
|
'modloader': 'fabric',
|
||||||
|
'description': data.get('description') or '',
|
||||||
|
'environment': data.get('environment') or '',
|
||||||
|
'minecraft': depends.get('minecraft') or '',
|
||||||
|
'dependencies': deps,
|
||||||
|
}
|
||||||
|
if 'quilt.mod.json' in names:
|
||||||
|
data = _decode(zf.read('quilt.mod.json'))
|
||||||
|
ql = data.get('quilt_loader') or {}
|
||||||
|
meta = ql.get('metadata') or {}
|
||||||
|
deps = []
|
||||||
|
for d in ql.get('depends') or []:
|
||||||
|
deps.append({
|
||||||
|
'modId': d.get('id') or '',
|
||||||
|
'type': d.get('reason') or 'required',
|
||||||
|
'versionRange': d.get('versions') or '',
|
||||||
|
})
|
||||||
|
minecraft = next((d['versionRange'] for d in deps if d['modId'] == 'minecraft'), '')
|
||||||
|
return {
|
||||||
|
'id': ql.get('id') or '',
|
||||||
|
'name': meta.get('name') or ql.get('id') or '',
|
||||||
|
'version': ql.get('version') or '',
|
||||||
|
'modloader': 'quilt',
|
||||||
|
'description': meta.get('description') or '',
|
||||||
|
'environment': '',
|
||||||
|
'minecraft': minecraft,
|
||||||
|
'dependencies': deps,
|
||||||
|
}
|
||||||
|
for member, loader in (('META-INF/neoforge.mods.toml', 'neoforge'), ('META-INF/mods.toml', 'forge')):
|
||||||
|
if member in names:
|
||||||
|
parsed = _parse_mods_toml(zf.read(member).decode('utf-8', 'replace'), loader)
|
||||||
|
if parsed:
|
||||||
|
return parsed
|
||||||
|
if 'META-INF/MANIFEST.MF' in names:
|
||||||
|
raw = zf.read('META-INF/MANIFEST.MF').decode('utf-8', 'replace')
|
||||||
|
title = _manifest_value(raw, 'Implementation-Title')
|
||||||
|
version = _manifest_value(raw, 'Implementation-Version')
|
||||||
|
if title or version:
|
||||||
|
return {
|
||||||
|
'id': title or '',
|
||||||
|
'name': title or 'unknown',
|
||||||
|
'version': version or '',
|
||||||
|
'modloader': 'unknown',
|
||||||
|
'description': '',
|
||||||
|
'environment': '',
|
||||||
|
'minecraft': '',
|
||||||
|
'dependencies': [],
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
zf.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_mods_toml(text, loader):
|
||||||
|
"""Parse a Forge/NeoForge mods.toml / neoforge.mods.toml."""
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
data = tomllib.loads(text)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
mods = data.get('mods') or []
|
||||||
|
if not mods:
|
||||||
|
return None
|
||||||
|
m = mods[0]
|
||||||
|
mod_id = m.get('modId') or ''
|
||||||
|
deps = []
|
||||||
|
for d in (data.get('dependencies') or {}).get(mod_id) or []:
|
||||||
|
deps.append({
|
||||||
|
'modId': d.get('modId') or '',
|
||||||
|
'type': d.get('type') or 'required',
|
||||||
|
'versionRange': d.get('versionRange') or '',
|
||||||
|
})
|
||||||
|
minecraft = next((d['versionRange'] for d in deps if d['modId'] == 'minecraft'), '')
|
||||||
|
return {
|
||||||
|
'id': mod_id,
|
||||||
|
'name': m.get('displayName') or mod_id,
|
||||||
|
'version': m.get('version') or '',
|
||||||
|
'modloader': loader,
|
||||||
|
'description': m.get('description') or '',
|
||||||
|
'environment': '',
|
||||||
|
'minecraft': minecraft,
|
||||||
|
'dependencies': deps,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def read_mod_version_manifest(file_entries):
|
||||||
|
"""Build the Mods manifest for a mod-category version.
|
||||||
|
|
||||||
|
file_entries: list of (filename, path). Accepts standalone .jar files and
|
||||||
|
.zip archives whose root holds .jar members (subfolders are ignored).
|
||||||
|
Returns {mods: [...], no_mods: bool}.
|
||||||
|
"""
|
||||||
|
mods = []
|
||||||
|
for filename, path in file_entries:
|
||||||
|
lower = filename.lower()
|
||||||
|
if lower.endswith('.jar'):
|
||||||
|
meta = read_mod_manifest(path)
|
||||||
|
if meta:
|
||||||
|
if not meta.get('minecraft'):
|
||||||
|
meta['minecraft'] = _mc_from_filename(filename)
|
||||||
|
meta['file_name'] = filename
|
||||||
|
meta['member'] = ''
|
||||||
|
mods.append(meta)
|
||||||
|
elif lower.endswith('.zip'):
|
||||||
|
zf = _open_zip(path)
|
||||||
|
if zf is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
root_jars = sorted(n for n in zf.namelist() if n.lower().endswith('.jar') and '/' not in n)
|
||||||
|
for member in root_jars:
|
||||||
|
meta = read_mod_manifest(zf.read(member))
|
||||||
|
if meta:
|
||||||
|
if not meta.get('minecraft'):
|
||||||
|
meta['minecraft'] = _mc_from_filename(member)
|
||||||
|
meta['file_name'] = member
|
||||||
|
meta['member'] = member
|
||||||
|
mods.append(meta)
|
||||||
|
finally:
|
||||||
|
zf.close()
|
||||||
|
return {'mods': mods, 'no_mods': len(mods) == 0}
|
||||||
|
|
||||||
|
|
||||||
def _in_plugins_dir(name):
|
def _in_plugins_dir(name):
|
||||||
"""True when a member lives inside a plugins/ folder (any nesting)."""
|
"""True when a member lives inside a plugins/ folder (any nesting)."""
|
||||||
parts = name.split('/')
|
parts = name.split('/')
|
||||||
|
|||||||
@@ -3554,3 +3554,49 @@ a.deletelink {
|
|||||||
}
|
}
|
||||||
.skin-render-btn { width: 100%; }
|
.skin-render-btn { width: 100%; }
|
||||||
|
|
||||||
|
|
||||||
|
/* Mod Info tab */
|
||||||
|
.mods-warning {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: #fff4e0;
|
||||||
|
border: 1px solid #f0c878;
|
||||||
|
color: #7a4d00;
|
||||||
|
padding: 0.7rem 0.9rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 0.9rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.mod-card {
|
||||||
|
background: var(--md-sys-color-surface-variant, #45475a);
|
||||||
|
border: 1px solid var(--md-sys-color-outline-variant, #3a3f4d);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.9rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.mod-card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
color: var(--md-sys-color-on-surface, #cdd6f4);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.mod-loader-badge {
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #e0e7ff;
|
||||||
|
color: #3730a3;
|
||||||
|
padding: 0.1rem 0.55rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.mod-version { color: var(--md-sys-color-on-surface-variant, #a6adc8); font-size: 0.85rem; }
|
||||||
|
.mod-id { margin: 0.3rem 0 0; color: var(--md-sys-color-on-surface-variant, #a6adc8); font-size: 0.78rem; }
|
||||||
|
.mod-desc { margin: 0.4rem 0; color: var(--md-sys-color-on-surface, #cdd6f4); font-size: 0.88rem; }
|
||||||
|
.mod-meta { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; color: var(--md-sys-color-on-surface-variant, #a6adc8); font-size: 0.78rem; }
|
||||||
|
.mod-deps { list-style: none; margin: 0.5rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.8rem; }
|
||||||
|
.mod-deps li { color: var(--md-sys-color-on-surface-variant, #a6adc8); }
|
||||||
|
.mod-deps code { background: #313244; color: #cdd6f4; padding: 0 0.3rem; border-radius: 4px; }
|
||||||
|
.mod-dep-type { color: #8a94b0; font-size: 0.72rem; }
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
{% for loader in mods_manifest.modloaders %}<span class="category-badge modpack-badge" title="Modloader">{{ loader }}</span>{% endfor %}
|
{% 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>
|
<span class="category-badge modpack-badge" title="Mod count">{{ mods_manifest.totals.total }} mods</span>
|
||||||
{% endif %}
|
{% 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>
|
<span class="project-id-badge" title="Project ID">#{{ project.pk }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% if project.summary %}<p class="project-summary">{{ project.summary }}</p>{% endif %}
|
{% if project.summary %}<p class="project-summary">{{ project.summary }}</p>{% endif %}
|
||||||
@@ -75,6 +79,7 @@
|
|||||||
{% if mods_manifest %}<button class="tab-btn" data-tab="mods" role="tab">Mods</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 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 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="gallery" role="tab">Gallery</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -453,6 +458,42 @@
|
|||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% 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">
|
<section class="tab-panel" id="tab-gallery">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2><i class="fas fa-images"></i> Gallery</h2> <div class="gallery-grid">
|
<h2><i class="fas fa-images"></i> Gallery</h2> <div class="gallery-grid">
|
||||||
|
|||||||
Reference in New Issue
Block a user