Initial commit - LEGO Instructions Manager v1.5.0
This commit is contained in:
415
app/templates/sets/detail.html
Normal file
415
app/templates/sets/detail.html
Normal file
@@ -0,0 +1,415 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ set.set_number }}: {{ set.set_name }} - {{ app_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('main.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('sets.list_sets') }}">Sets</a></li>
|
||||
<li class="breadcrumb-item active">{{ set.set_number }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<!-- Set Image and Details -->
|
||||
<div class="col-lg-4 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
{% if set.cover_image %}
|
||||
<img src="{{ url_for('static', filename='uploads/' + set.cover_image.replace('\\', '/')) }}"
|
||||
class="card-img-top" alt="{{ set.set_name }}"
|
||||
style="max-height: 400px; object-fit: contain; background-color: #f8f9fa; padding: 20px;">
|
||||
{% elif set.image_url %}
|
||||
<img src="{{ set.image_url }}" class="card-img-top" alt="{{ set.set_name }}"
|
||||
style="max-height: 400px; object-fit: contain; background-color: #f8f9fa; padding: 20px;">
|
||||
{% else %}
|
||||
<div class="d-flex align-items-center justify-content-center bg-light" style="height: 400px;">
|
||||
<i class="bi bi-image display-1 text-muted"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
{{ set.set_number }}
|
||||
{% if set.is_moc %}
|
||||
<span class="badge bg-warning text-dark">
|
||||
<i class="bi bi-star-fill"></i> MOC
|
||||
</span>
|
||||
{% endif %}
|
||||
</h5>
|
||||
<h6 class="card-subtitle mb-3 text-muted">{{ set.set_name }}</h6>
|
||||
|
||||
<table class="table table-sm table-borderless">
|
||||
<tr>
|
||||
<th width="40%">Theme:</th>
|
||||
<td><span class="badge bg-primary">{{ set.theme }}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Year:</th>
|
||||
<td><span class="badge bg-warning text-dark">{{ set.year_released }}</span></td>
|
||||
</tr>
|
||||
{% if set.is_moc %}
|
||||
<tr>
|
||||
<th>Type:</th>
|
||||
<td><span class="badge bg-info">My Own Creation</span></td>
|
||||
</tr>
|
||||
{% if set.moc_designer %}
|
||||
<tr>
|
||||
<th>Designer:</th>
|
||||
<td>{{ set.moc_designer }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if set.piece_count %}
|
||||
<tr>
|
||||
<th>Pieces:</th>
|
||||
<td>{{ set.piece_count }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>Instructions:</th>
|
||||
<td>{{ set.instructions.count() }} file(s)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Added:</th>
|
||||
<td>{{ set.created_at.strftime('%b %d, %Y') }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-transparent">
|
||||
<div class="d-grid gap-2">
|
||||
<a href="{{ url_for('instructions.upload', set_id=set.id) }}" class="btn btn-success">
|
||||
<i class="bi bi-cloud-upload"></i> Upload Instructions
|
||||
</a>
|
||||
<a href="{{ url_for('sets.edit_set', set_id=set.id) }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-pencil"></i> Edit Set Details
|
||||
</a>
|
||||
<form method="POST" action="{{ url_for('sets.delete_set', set_id=set.id) }}"
|
||||
onsubmit="return confirm('Are you sure you want to delete this set and all its instructions?');">
|
||||
<button type="submit" class="btn btn-outline-danger w-100">
|
||||
<i class="bi bi-trash"></i> Delete Set
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instructions -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-file-pdf"></i> Instructions</h5>
|
||||
<a href="{{ url_for('instructions.upload', set_id=set.id) }}" class="btn btn-sm btn-success">
|
||||
<i class="bi bi-plus-circle"></i> Upload
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if pdf_instructions or image_instructions %}
|
||||
|
||||
<!-- PDF Instructions (Books) -->
|
||||
{% if pdf_instructions %}
|
||||
<h6 class="mb-3"><i class="bi bi-book-fill text-danger"></i> PDF Instruction Books</h6>
|
||||
<div class="row mb-4">
|
||||
{% for instruction in pdf_instructions %}
|
||||
<div class="col-md-6 col-lg-4 mb-3">
|
||||
<div class="card h-100">
|
||||
{% if instruction.thumbnail_path %}
|
||||
<a href="{{ url_for('instructions.view', instruction_id=instruction.id) }}" target="_blank">
|
||||
<img src="{{ url_for('static', filename='uploads/' + instruction.thumbnail_path.replace('\\', '/')) }}"
|
||||
class="card-img-top"
|
||||
alt="{{ instruction.file_name }}"
|
||||
style="height: 200px; object-fit: contain; background-color: #f8f9fa; padding: 10px;">
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('instructions.view', instruction_id=instruction.id) }}" target="_blank">
|
||||
<div class="card-img-top d-flex align-items-center justify-content-center bg-light"
|
||||
style="height: 200px;">
|
||||
<i class="bi bi-file-pdf display-1 text-danger"></i>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{ instruction.file_name }}</h6>
|
||||
<p class="card-text small text-muted">
|
||||
<i class="bi bi-file-pdf"></i> {{ instruction.file_size_mb }} MB<br>
|
||||
<i class="bi bi-calendar"></i> {{ instruction.uploaded_at.strftime('%b %d, %Y') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent">
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ url_for('instructions.view', instruction_id=instruction.id) }}"
|
||||
class="btn btn-sm btn-primary flex-fill" target="_blank">
|
||||
<i class="bi bi-eye"></i> Open
|
||||
</a>
|
||||
<form method="POST" action="{{ url_for('instructions.delete', instruction_id=instruction.id) }}"
|
||||
onsubmit="return confirm('Delete this PDF?');" class="flex-fill">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger w-100">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Image Instructions (Single Card) -->
|
||||
{% if image_instructions %}
|
||||
<h6 class="mb-3"><i class="bi bi-images text-primary"></i> Scanned Instructions</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-lg-4 mb-3">
|
||||
<div class="card h-100">
|
||||
{% set first_image = image_instructions[0] %}
|
||||
<a href="{{ url_for('instructions.image_viewer', set_id=set.id) }}">
|
||||
<img src="{{ url_for('static', filename='uploads/' + first_image.file_path.replace('\\', '/')) }}"
|
||||
class="card-img-top"
|
||||
alt="Instructions Preview"
|
||||
style="height: 200px; object-fit: contain; background-color: #f8f9fa; padding: 10px; cursor: pointer;">
|
||||
</a>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">
|
||||
<i class="bi bi-file-image"></i> Image Instructions
|
||||
</h6>
|
||||
<p class="card-text small text-muted">
|
||||
<i class="bi bi-files"></i> {{ image_instructions|length }} page(s)<br>
|
||||
<i class="bi bi-calendar"></i> Uploaded {{ first_image.uploaded_at.strftime('%b %d, %Y') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent">
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ url_for('instructions.image_viewer', set_id=set.id) }}"
|
||||
class="btn btn-sm btn-primary flex-fill">
|
||||
<i class="bi bi-book-half"></i> View Instructions
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger flex-fill"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteImagesModal">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete All Images Modal -->
|
||||
<div class="modal fade" id="deleteImagesModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Delete All Image Instructions?</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>This will delete all {{ image_instructions|length }} image instruction pages.</p>
|
||||
<p class="text-danger"><strong>This cannot be undone!</strong></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<form method="POST" action="{{ url_for('instructions.delete_all_images', set_id=set.id) }}" style="display: inline;">
|
||||
<button type="submit" class="btn btn-danger">Delete All Images</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-file-earmark-x display-1 text-muted"></i>
|
||||
<h5 class="mt-3">No Instructions Yet</h5>
|
||||
<p class="text-muted">Upload PDF or image files to get started.</p>
|
||||
<a href="{{ url_for('instructions.upload', set_id=set.id) }}" class="btn btn-success">
|
||||
<i class="bi bi-cloud-upload"></i> Upload Instructions
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Extra Files Section -->
|
||||
<div class="card shadow-sm mt-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-file-earmark-plus"></i> Extra Files</h5>
|
||||
<a href="{{ url_for('extra_files.upload', set_id=set.id) }}" class="btn btn-sm btn-success">
|
||||
<i class="bi bi-cloud-upload"></i> Upload Files
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% set extra_files_list = set.extra_files.all() %}
|
||||
{% if extra_files_list %}
|
||||
<!-- Files Grid -->
|
||||
<div class="row g-3">
|
||||
{% for file in extra_files_list %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card h-100 border-light">
|
||||
<!-- Preview for images -->
|
||||
{% if file.is_image %}
|
||||
<a href="{{ url_for('extra_files.preview', file_id=file.id) }}" target="_blank">
|
||||
<img src="{{ url_for('extra_files.preview', file_id=file.id) }}"
|
||||
class="card-img-top"
|
||||
alt="{{ file.original_filename }}"
|
||||
style="height: 150px; object-fit: cover; cursor: pointer;">
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="card-img-top bg-light d-flex align-items-center justify-content-center"
|
||||
style="height: 150px;">
|
||||
<i class="bi bi-{{ file.file_icon }} display-3 text-muted"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-body p-2">
|
||||
<h6 class="card-title small mb-1">
|
||||
<i class="bi bi-{{ file.file_icon }}"></i>
|
||||
{{ file.original_filename }}
|
||||
</h6>
|
||||
|
||||
{% if file.category and file.category != 'other' %}
|
||||
<span class="badge bg-info text-dark small mb-1">
|
||||
{{ file.category|replace('_', ' ')|title }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<p class="card-text small text-muted mb-1">
|
||||
<i class="bi bi-hdd"></i> {{ file.file_size_formatted }}
|
||||
<br>
|
||||
<i class="bi bi-calendar"></i> {{ file.uploaded_at.strftime('%b %d, %Y') }}
|
||||
</p>
|
||||
|
||||
{% if file.description %}
|
||||
<p class="card-text small text-muted mb-1">
|
||||
{{ file.description|truncate(60) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-transparent p-2">
|
||||
<div class="d-flex gap-1">
|
||||
{% if file.can_preview %}
|
||||
<a href="{{ url_for('extra_files.preview', file_id=file.id) }}"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-outline-primary flex-fill"
|
||||
title="Preview">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('extra_files.download', file_id=file.id) }}"
|
||||
class="btn btn-sm btn-outline-success flex-fill"
|
||||
title="Download">
|
||||
<i class="bi bi-download"></i>
|
||||
</a>
|
||||
<form method="POST"
|
||||
action="{{ url_for('extra_files.delete', file_id=file.id) }}"
|
||||
class="flex-fill"
|
||||
onsubmit="return confirm('Delete {{ file.original_filename }}?');">
|
||||
<button type="submit"
|
||||
class="btn btn-sm btn-outline-danger w-100"
|
||||
title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-4">
|
||||
<i class="bi bi-files display-3 text-muted"></i>
|
||||
<p class="text-muted mt-2 mb-1">No extra files yet</p>
|
||||
<p class="small text-muted">
|
||||
Upload BrickLink XMLs, Stud.io files, box art, photos, or any other related files
|
||||
</p>
|
||||
<a href="{{ url_for('extra_files.upload', set_id=set.id) }}" class="btn btn-sm btn-success">
|
||||
<i class="bi bi-cloud-upload"></i> Upload Files
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Set Information -->
|
||||
<div class="card shadow-sm mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Additional Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-4">Set Number:</dt>
|
||||
<dd class="col-sm-8">{{ set.set_number }}</dd>
|
||||
|
||||
<dt class="col-sm-4">Set Name:</dt>
|
||||
<dd class="col-sm-8">{{ set.set_name }}</dd>
|
||||
|
||||
<dt class="col-sm-4">Theme:</dt>
|
||||
<dd class="col-sm-8">{{ set.theme }}</dd>
|
||||
|
||||
<dt class="col-sm-4">Year Released:</dt>
|
||||
<dd class="col-sm-8">{{ set.year_released }}</dd>
|
||||
|
||||
{% if set.piece_count %}
|
||||
<dt class="col-sm-4">Piece Count:</dt>
|
||||
<dd class="col-sm-8">{{ set.piece_count }} pieces</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if set.is_moc %}
|
||||
<dt class="col-sm-4">Type:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span class="badge bg-warning text-dark">
|
||||
<i class="bi bi-star-fill"></i> My Own Creation (MOC)
|
||||
</span>
|
||||
</dd>
|
||||
|
||||
{% if set.moc_designer %}
|
||||
<dt class="col-sm-4">Designer:</dt>
|
||||
<dd class="col-sm-8">{{ set.moc_designer }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if set.moc_description %}
|
||||
<dt class="col-sm-4">Description:</dt>
|
||||
<dd class="col-sm-8">{{ set.moc_description }}</dd>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if set.brickset_id %}
|
||||
<dt class="col-sm-4">Brickset ID:</dt>
|
||||
<dd class="col-sm-8">{{ set.brickset_id }}</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt class="col-sm-4">Added By:</dt>
|
||||
<dd class="col-sm-8">{{ set.added_by.username }}</dd>
|
||||
|
||||
<dt class="col-sm-4">Date Added:</dt>
|
||||
<dd class="col-sm-8">{{ set.created_at.strftime('%B %d, %Y at %I:%M %p') }}</dd>
|
||||
|
||||
<dt class="col-sm-4">Last Updated:</dt>
|
||||
<dd class="col-sm-8">{{ set.updated_at.strftime('%B %d, %Y at %I:%M %p') }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{{ url_for('sets.list_sets') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to Sets
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.instruction-thumbnail {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.instruction-thumbnail:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user