finished Skin UGC
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-08-05 14:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('library', '0009_version_plugins_manifest'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='version',
|
||||||
|
name='skins_manifest',
|
||||||
|
field=models.JSONField(blank=True, default=dict),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -210,6 +210,7 @@ class Version(models.Model):
|
|||||||
pack_description = models.TextField(blank=True, default='')
|
pack_description = models.TextField(blank=True, default='')
|
||||||
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)
|
||||||
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)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ from .zips import (
|
|||||||
read_pack_meta,
|
read_pack_meta,
|
||||||
read_pack_mcmeta,
|
read_pack_mcmeta,
|
||||||
read_plugins_manifest,
|
read_plugins_manifest,
|
||||||
|
read_skins_manifest,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -333,6 +334,7 @@ def project_detail(request, slug):
|
|||||||
logical_manifest = None
|
logical_manifest = None
|
||||||
mods_manifest = None
|
mods_manifest = None
|
||||||
plugins_manifest = None
|
plugins_manifest = None
|
||||||
|
skins_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:
|
||||||
@@ -343,6 +345,7 @@ def project_detail(request, slug):
|
|||||||
logical_manifest = latest.logical_manifest or None
|
logical_manifest = latest.logical_manifest or None
|
||||||
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
|
||||||
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')
|
||||||
@@ -376,6 +379,7 @@ def project_detail(request, slug):
|
|||||||
'logical_manifest': logical_manifest,
|
'logical_manifest': logical_manifest,
|
||||||
'mods_manifest': mods_manifest,
|
'mods_manifest': mods_manifest,
|
||||||
'plugins_manifest': plugins_manifest,
|
'plugins_manifest': plugins_manifest,
|
||||||
|
'skins_manifest': skins_manifest,
|
||||||
'autoload_plugins_json': autoload_plugins_json,
|
'autoload_plugins_json': autoload_plugins_json,
|
||||||
'anim_stats': anim_stats,
|
'anim_stats': anim_stats,
|
||||||
}
|
}
|
||||||
@@ -652,6 +656,7 @@ def _reparse_version(version):
|
|||||||
version.models_manifest = read_models_manifest(path, version.project.category) or {}
|
version.models_manifest = read_models_manifest(path, version.project.category) or {}
|
||||||
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 {}
|
||||||
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)
|
||||||
@@ -674,6 +679,7 @@ def _finalize_version(project, version_name, changelog, temp_uploads, actor):
|
|||||||
pack_description = ''
|
pack_description = ''
|
||||||
mods_manifest = {}
|
mods_manifest = {}
|
||||||
plugins_manifest = None
|
plugins_manifest = None
|
||||||
|
skins_manifest = None
|
||||||
animation_id = ''
|
animation_id = ''
|
||||||
animation_manifest = None
|
animation_manifest = None
|
||||||
models_manifest = None
|
models_manifest = None
|
||||||
@@ -718,6 +724,8 @@ def _finalize_version(project, version_name, changelog, temp_uploads, actor):
|
|||||||
mods_manifest = manifest
|
mods_manifest = manifest
|
||||||
if plugins_manifest is None and project.category == 'model':
|
if plugins_manifest is None and project.category == 'model':
|
||||||
plugins_manifest = read_plugins_manifest(path)
|
plugins_manifest = read_plugins_manifest(path)
|
||||||
|
if skins_manifest is None and project.category == 'skin':
|
||||||
|
skins_manifest = read_skins_manifest(path)
|
||||||
|
|
||||||
update_fields = []
|
update_fields = []
|
||||||
if pack_format is not None:
|
if pack_format is not None:
|
||||||
@@ -730,6 +738,9 @@ def _finalize_version(project, version_name, changelog, temp_uploads, actor):
|
|||||||
if plugins_manifest is not None:
|
if plugins_manifest is not None:
|
||||||
version.plugins_manifest = plugins_manifest
|
version.plugins_manifest = plugins_manifest
|
||||||
update_fields.append('plugins_manifest')
|
update_fields.append('plugins_manifest')
|
||||||
|
if skins_manifest is not None:
|
||||||
|
version.skins_manifest = skins_manifest
|
||||||
|
update_fields.append('skins_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')
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ adopted file (under MEDIA_ROOT) and return plain dicts/values.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import struct
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
INJECTOR_NAMES = {'V': 'Vaginal', 'M': 'Mouth', 'A': 'Anal'}
|
INJECTOR_NAMES = {'V': 'Vaginal', 'M': 'Mouth', 'A': 'Anal'}
|
||||||
@@ -16,6 +17,48 @@ PROBLEM_TAGS = {'broken', 'bugged', 'borked'}
|
|||||||
MODEL_FILE_EXTENSIONS = ('.bbmodel', '.geo.json', '.jem', '.gltf', '.glb')
|
MODEL_FILE_EXTENSIONS = ('.bbmodel', '.geo.json', '.jem', '.gltf', '.glb')
|
||||||
|
|
||||||
|
|
||||||
|
def _png_size(head):
|
||||||
|
"""(width, height) from a PNG IHDR header, or None when not a PNG."""
|
||||||
|
if len(head) < 24 or head[:8] != b'\x89PNG\r\n\x1a\n':
|
||||||
|
return None
|
||||||
|
return (struct.unpack('>I', head[16:20])[0], struct.unpack('>I', head[20:24])[0])
|
||||||
|
|
||||||
|
|
||||||
|
SKIN_SIZES = {(64, 32), (64, 64), (128, 128)}
|
||||||
|
|
||||||
|
|
||||||
|
def read_skins_manifest(path):
|
||||||
|
"""Scan the archive for Minecraft skin PNGs (64x32, 64x64 or 128x128).
|
||||||
|
|
||||||
|
Returns None when no skins are found; otherwise {skins: [{name, member,
|
||||||
|
width, height}]} where ``member`` is the zip member served via pack_asset.
|
||||||
|
"""
|
||||||
|
zf = _open_zip(path)
|
||||||
|
if zf is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
skins = []
|
||||||
|
for name in sorted(n for n in set(zf.namelist()) if n.lower().endswith('.png')):
|
||||||
|
try:
|
||||||
|
with zf.open(name) as fh:
|
||||||
|
head = fh.read(24)
|
||||||
|
except (KeyError, OSError, zipfile.BadZipFile):
|
||||||
|
continue
|
||||||
|
size = _png_size(head)
|
||||||
|
if size and size in SKIN_SIZES:
|
||||||
|
skins.append({
|
||||||
|
'name': name.rsplit('/', 1)[-1],
|
||||||
|
'member': name,
|
||||||
|
'width': size[0],
|
||||||
|
'height': size[1],
|
||||||
|
})
|
||||||
|
if not skins:
|
||||||
|
return None
|
||||||
|
return {'skins': skins}
|
||||||
|
finally:
|
||||||
|
zf.close()
|
||||||
|
|
||||||
|
|
||||||
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('/')
|
||||||
|
|||||||
@@ -3484,3 +3484,73 @@ a.deletelink {
|
|||||||
padding: 0.1rem 0.5rem; font-size: 0.68rem; font-weight: 600; text-transform: uppercase;
|
padding: 0.1rem 0.5rem; font-size: 0.68rem; font-weight: 600; text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.plugins-size { color: var(--md-sys-color-on-surface-variant, #6b7280); font-size: 0.78rem; }
|
.plugins-size { color: var(--md-sys-color-on-surface-variant, #6b7280); font-size: 0.78rem; }
|
||||||
|
|
||||||
|
/* Skins tab */
|
||||||
|
.skin-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.skin-card {
|
||||||
|
background: var(--md-sys-color-surface-variant, #45475a);
|
||||||
|
border: 1px solid var(--md-sys-color-outline-variant, #3a3f4d);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
.skin-preview {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
object-fit: contain;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
background: repeating-conic-gradient(#2a2e3a 0 25%, #232732 0 50%) 0 0 / 24px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.skin-card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--md-sys-color-on-surface, #cdd6f4);
|
||||||
|
}
|
||||||
|
.skin-card-head strong {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.skin-size-badge {
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #313244;
|
||||||
|
color: #a6adc8;
|
||||||
|
padding: 0.1rem 0.5rem;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 600;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.skin-card-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
.skin-model-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--md-sys-color-on-surface-variant, #a6adc8);
|
||||||
|
}
|
||||||
|
.skin-model-select {
|
||||||
|
background: var(--md-sys-color-surface, #1e1e2e);
|
||||||
|
color: var(--md-sys-color-on-surface, #cdd6f4);
|
||||||
|
border: 1px solid var(--md-sys-color-outline-variant, #45475a);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.15rem 0.3rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
.skin-render-btn { width: 100%; }
|
||||||
|
|
||||||
|
|||||||
@@ -173,15 +173,9 @@
|
|||||||
return baseTextures.concat(featureTextures);
|
return baseTextures.concat(featureTextures);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function open(btn) {
|
// Create (or reset) the full-screen Blockbench overlay + fresh iframe and
|
||||||
const member = btn.dataset.member;
|
// wait for the app to be ready. Returns false on failure.
|
||||||
const name = btn.dataset.name || member;
|
async function prepareOverlay(name) {
|
||||||
const modelFormat = btn.dataset.modelFormat || '.geo.json';
|
|
||||||
const slug = btn.dataset.projectSlug || '';
|
|
||||||
const opts = { baseUrl: btn.dataset.baseUrl, vanillaBaseUrl: btn.dataset.vanillaBaseUrl, defaultTexture: btn.dataset.defaultTexture || null };
|
|
||||||
if (!member || !opts.baseUrl || busy) return;
|
|
||||||
busy = true;
|
|
||||||
|
|
||||||
const overlayEl = ensureOverlay();
|
const overlayEl = ensureOverlay();
|
||||||
overlayEl.hidden = false;
|
overlayEl.hidden = false;
|
||||||
document.querySelector('#bb-overlay-title').textContent = name;
|
document.querySelector('#bb-overlay-title').textContent = name;
|
||||||
@@ -209,13 +203,21 @@
|
|||||||
};
|
};
|
||||||
window.addEventListener('message', onMsg);
|
window.addEventListener('message', onMsg);
|
||||||
});
|
});
|
||||||
|
if (!ready) setStatus('Blockbench failed to start.');
|
||||||
if (!ready) {
|
return ready;
|
||||||
setStatus('Blockbench failed to start.');
|
|
||||||
busy = false;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function open(btn) {
|
||||||
|
const member = btn.dataset.member;
|
||||||
|
const name = btn.dataset.name || member;
|
||||||
|
const modelFormat = btn.dataset.modelFormat || '.geo.json';
|
||||||
|
const slug = btn.dataset.projectSlug || '';
|
||||||
|
const opts = { baseUrl: btn.dataset.baseUrl, vanillaBaseUrl: btn.dataset.vanillaBaseUrl, defaultTexture: btn.dataset.defaultTexture || null };
|
||||||
|
if (!member || !opts.baseUrl || busy) return;
|
||||||
|
busy = true;
|
||||||
|
|
||||||
|
if (!(await prepareOverlay(name))) { busy = false; return; }
|
||||||
|
|
||||||
setStatus('Loading model…');
|
setStatus('Loading model…');
|
||||||
try {
|
try {
|
||||||
const msg = { type: 'packs-open-model', name: name };
|
const msg = { type: 'packs-open-model', name: name };
|
||||||
@@ -254,6 +256,34 @@
|
|||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load a Minecraft skin into Blockbench's built-in "Minecraft Skin" project.
|
||||||
|
async function openSkin(btn) {
|
||||||
|
const member = btn.dataset.member;
|
||||||
|
const name = btn.dataset.name || member;
|
||||||
|
const opts = { baseUrl: btn.dataset.baseUrl };
|
||||||
|
if (!member || !opts.baseUrl || busy) return;
|
||||||
|
busy = true;
|
||||||
|
|
||||||
|
if (!(await prepareOverlay(name))) { busy = false; return; }
|
||||||
|
|
||||||
|
setStatus('Loading skin…');
|
||||||
|
try {
|
||||||
|
const select = btn.closest('.skin-card')?.querySelector('.skin-model-select');
|
||||||
|
const model = select ? select.value : 'steve';
|
||||||
|
const img = await loadImage(assetUrl(opts.baseUrl, member));
|
||||||
|
iframe.contentWindow.postMessage({
|
||||||
|
type: 'packs-open-skin',
|
||||||
|
name: name,
|
||||||
|
model: model,
|
||||||
|
dataUrl: imageDataUrl(img),
|
||||||
|
}, window.location.origin);
|
||||||
|
setStatus('Applying…');
|
||||||
|
} catch (e) {
|
||||||
|
setStatus('Could not load skin: ' + e.message);
|
||||||
|
}
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('message', (e) => {
|
window.addEventListener('message', (e) => {
|
||||||
if (e.origin !== window.location.origin) return;
|
if (e.origin !== window.location.origin) return;
|
||||||
if (!e.data) return;
|
if (!e.data) return;
|
||||||
@@ -266,5 +296,5 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.PacksPreview = { open: open, close: close };
|
window.PacksPreview = { open: open, openSkin: openSkin, close: close };
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -124,8 +124,45 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open a Minecraft Skin project (Steve/Alex player model) with the skin.
|
||||||
|
async function openSkin(msg) {
|
||||||
|
await waitFor(() => typeof Formats !== 'undefined' && !!Formats.skin, 15000);
|
||||||
|
const model = msg.model === 'alex' ? 'alex' : 'steve';
|
||||||
|
const dialog = Formats.skin.setup_dialog;
|
||||||
|
dialog.show();
|
||||||
|
await dialog.setFormValues({
|
||||||
|
model: model,
|
||||||
|
texture_source: 'upload_texture',
|
||||||
|
texture_file: { content: msg.dataUrl, name: 'skin.png', path: 'skin.png' },
|
||||||
|
layer_template: true,
|
||||||
|
});
|
||||||
|
dialog.confirm();
|
||||||
|
// Reveal every cube (incl. the outer layer) and land in Pose mode.
|
||||||
|
await new Promise((r) => setTimeout(r, 700));
|
||||||
|
try {
|
||||||
|
(typeof Cube !== 'undefined' && Cube.all || []).forEach((c) => { c.visibility = true; });
|
||||||
|
if (typeof Canvas !== 'undefined' && Canvas.updateAll) Canvas.updateAll();
|
||||||
|
const pose = Modes && Modes.options && Modes.options.pose;
|
||||||
|
if (pose && typeof pose.select === 'function') pose.select();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('packs skin post-load', e);
|
||||||
|
}
|
||||||
|
notify('model-open', { name: msg.name || 'skin' });
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('message', (e) => {
|
window.addEventListener('message', (e) => {
|
||||||
if (!e.data || e.data.type !== 'packs-open-model') return;
|
if (!e.data) return;
|
||||||
|
if (e.data.type === 'packs-open-skin') {
|
||||||
|
openSkin(e.data).then(
|
||||||
|
() => {},
|
||||||
|
(err) => {
|
||||||
|
console.error('packs-open-skin failed', err);
|
||||||
|
notify('model-error', { name: e.data.name || '', error: String(err) });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.data.type !== 'packs-open-model') return;
|
||||||
openModel(e.data).then(
|
openModel(e.data).then(
|
||||||
() => {},
|
() => {},
|
||||||
(err) => {
|
(err) => {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
{% if logical_manifest %}<button class="tab-btn" data-tab="logical" role="tab">Logical</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 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 %}
|
||||||
<button class="tab-btn" data-tab="gallery" role="tab">Gallery</button>
|
<button class="tab-btn" data-tab="gallery" role="tab">Gallery</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -421,6 +422,37 @@
|
|||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% 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="{% url 'library:pack_asset' project.slug versions.0.pk skin.member %}" 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-member="{{ skin.member }}" data-name="{{ skin.name }}"
|
||||||
|
data-base-url="{% url 'library:pack_asset' project.slug versions.0.pk 'ASSET' %}"><i class="fas fa-user"></i> Render</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
@@ -454,7 +486,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
{% if models_manifest %}
|
{% if models_manifest or skins_manifest %}
|
||||||
<script src="{% static 'js/packs_preview.js' %}"></script>
|
<script src="{% static 'js/packs_preview.js' %}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<script>
|
<script>
|
||||||
@@ -555,6 +587,9 @@
|
|||||||
document.querySelectorAll('.model-render-btn').forEach(btn => {
|
document.querySelectorAll('.model-render-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => window.PacksPreview.open(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.
|
// Live filter for the Mods tab.
|
||||||
|
|||||||
Reference in New Issue
Block a user