262 lines
10 KiB
Twig
262 lines
10 KiB
Twig
{% extends 'base.html.twig' %}
|
|
{% block title %}{{ isEdit ? 'Edit Album' : 'Create Album' }}{% endblock %}
|
|
{% block body %}
|
|
<h1 class="h4 mb-3">{{ isEdit ? 'Edit album' : 'Create album' }}</h1>
|
|
<style>
|
|
.album-editor-cover {
|
|
aspect-ratio: 1 / 1;
|
|
width: 100%;
|
|
border: 2px dashed #d29ab2;
|
|
border-radius: 1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #faf6f8;
|
|
color: #8a4f69;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.album-editor-cover:hover {
|
|
border-color: #b55b80;
|
|
background: #f6eef2;
|
|
}
|
|
|
|
.album-editor-cover__preview {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.track-col-disc,
|
|
.track-col-number {
|
|
width: 110px;
|
|
}
|
|
|
|
.track-col-duration {
|
|
width: 170px;
|
|
}
|
|
|
|
.track-meta-input {
|
|
min-height: 2.8rem;
|
|
text-align: center;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.track-title-input,
|
|
.track-duration-input {
|
|
min-height: 2.8rem;
|
|
}
|
|
</style>
|
|
|
|
{{ form_start(form, {attr: {class: 'vstack gap-4', novalidate: 'novalidate'}}) }}
|
|
{{ form_errors(form) }}
|
|
|
|
<div class="row g-4 mb-2">
|
|
<div class="col-md-4">
|
|
<div class="card h-100">
|
|
<div class="card-body vstack gap-3">
|
|
<div>
|
|
{{ form_widget(form.coverUpload, {attr: {class: 'd-none', accept: 'image/*'}}) }}
|
|
{% set has_existing_cover = existingCoverUrl is defined and existingCoverUrl %}
|
|
<label for="{{ form.coverUpload.vars.id }}" class="album-editor-cover" id="cover-upload-trigger">
|
|
<img
|
|
id="cover-upload-preview"
|
|
class="album-editor-cover__preview{{ has_existing_cover ? '' : ' d-none' }}"
|
|
{% if has_existing_cover %}src="{{ existingCoverUrl }}"{% endif %}
|
|
alt="Album cover preview"
|
|
>
|
|
<span id="cover-upload-placeholder" class="{{ has_existing_cover ? 'd-none' : '' }}">Upload Image</span>
|
|
</label>
|
|
{{ form_errors(form.coverUpload) }}
|
|
</div>
|
|
<div>
|
|
{{ form_label(form.name) }}
|
|
{{ form_widget(form.name, {attr: {class: 'form-control'}}) }}
|
|
{{ form_errors(form.name) }}
|
|
</div>
|
|
<div>
|
|
{{ form_label(form.artistsCsv) }}
|
|
{{ form_widget(form.artistsCsv, {attr: {class: 'form-control'}}) }}
|
|
{{ form_errors(form.artistsCsv) }}
|
|
</div>
|
|
<div>
|
|
{{ form_label(form.releaseDate) }}
|
|
{{ form_widget(form.releaseDate, {attr: {class: 'form-control'}}) }}
|
|
{{ form_errors(form.releaseDate) }}
|
|
</div>
|
|
<div>
|
|
{{ form_label(form.genresCsv) }}
|
|
{{ form_widget(form.genresCsv, {attr: {class: 'form-control'}}) }}
|
|
{{ form_errors(form.genresCsv) }}
|
|
</div>
|
|
<div>
|
|
{{ form_label(form.externalUrl) }}
|
|
{{ form_widget(form.externalUrl, {attr: {class: 'form-control', placeholder: 'https://...'}}) }}
|
|
{{ form_errors(form.externalUrl) }}
|
|
</div>
|
|
<div class="d-none">
|
|
{{ form_widget(form.totalTracks) }}
|
|
{{ form_errors(form.totalTracks) }}
|
|
</div>
|
|
{% if isEdit and albumOwner %}
|
|
<small class="text-secondary">
|
|
Owner: {{ albumOwner.displayName ?? albumOwner.userIdentifier }}
|
|
</small>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-8">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h2 class="h5 mb-0">Tracklist</h2>
|
|
<span class="text-secondary"><span id="track-count">{{ trackRows|filter(t => t.title is not empty)|length }}</span> tracks</span>
|
|
</div>
|
|
<div class="mui-table-wrapper w-100">
|
|
<table class="mui-table mui-table--compact mui-table--striped mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Disc</th>
|
|
<th scope="col">#</th>
|
|
<th scope="col">Title</th>
|
|
<th scope="col" class="text-end">Duration</th>
|
|
<th scope="col"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="track-editor-body">
|
|
{% for track in trackRows %}
|
|
<tr>
|
|
<td class="mui-table__number track-col-disc">
|
|
<input class="form-control track-meta-input" type="text" inputmode="numeric" name="track_disc[]" value="{{ track.disc }}">
|
|
</td>
|
|
<td class="mui-table__number track-col-number">
|
|
<input class="form-control track-meta-input" type="text" inputmode="numeric" name="track_number[]" value="{{ track.track }}">
|
|
</td>
|
|
<td>
|
|
<input class="form-control track-title-input" type="text" name="track_title[]" value="{{ track.title }}" maxlength="512" placeholder="Track title">
|
|
</td>
|
|
<td class="text-end track-col-duration">
|
|
<input class="form-control track-duration-input text-end" type="text" inputmode="numeric" name="track_duration[]" value="{{ track.duration }}" placeholder="m:ss">
|
|
</td>
|
|
<td class="text-end" style="width:1%">
|
|
<button type="button" class="btn btn-outline-danger btn-sm track-remove-btn" title="Remove track">×</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="mt-3 d-flex gap-2">
|
|
<button type="button" id="add-track-row" class="btn btn-outline-secondary btn-sm">Add track</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-success" type="submit">{{ isEdit ? 'Save changes' : 'Create album' }}</button>
|
|
<a class="btn btn-link" href="{{ isEdit and albumId ? path('album_show', {id: albumId}) : path('album_search') }}">Cancel</a>
|
|
</div>
|
|
{{ form_end(form) }}
|
|
|
|
<template id="track-row-template">
|
|
<tr>
|
|
<td class="mui-table__number track-col-disc">
|
|
<input class="form-control track-meta-input" type="text" inputmode="numeric" name="track_disc[]" value="1">
|
|
</td>
|
|
<td class="mui-table__number track-col-number">
|
|
<input class="form-control track-meta-input" type="text" inputmode="numeric" name="track_number[]" value="1">
|
|
</td>
|
|
<td>
|
|
<input class="form-control track-title-input" type="text" name="track_title[]" value="" maxlength="512" placeholder="Track title">
|
|
</td>
|
|
<td class="text-end track-col-duration">
|
|
<input class="form-control track-duration-input text-end" type="text" inputmode="numeric" name="track_duration[]" value="" placeholder="m:ss">
|
|
</td>
|
|
<td class="text-end" style="width:1%">
|
|
<button type="button" class="btn btn-outline-danger btn-sm track-remove-btn" title="Remove track">×</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
|
|
<script>
|
|
(() => {
|
|
const trackBody = document.getElementById('track-editor-body');
|
|
const addBtn = document.getElementById('add-track-row');
|
|
const rowTemplate = document.getElementById('track-row-template');
|
|
const countNode = document.getElementById('track-count');
|
|
const totalTracksInput = document.getElementById('{{ form.totalTracks.vars.id }}');
|
|
const coverInput = document.getElementById('{{ form.coverUpload.vars.id }}');
|
|
const coverPreview = document.getElementById('cover-upload-preview');
|
|
const coverPlaceholder = document.getElementById('cover-upload-placeholder');
|
|
if (!trackBody || !addBtn || !rowTemplate || !countNode || !totalTracksInput) {
|
|
return;
|
|
}
|
|
|
|
const recalcTrackCount = () => {
|
|
const titles = Array.from(trackBody.querySelectorAll('.track-title-input'));
|
|
const count = titles.filter((input) => input.value.trim() !== '').length;
|
|
countNode.textContent = String(count);
|
|
totalTracksInput.value = String(count);
|
|
};
|
|
|
|
const bindRow = (row) => {
|
|
const removeBtn = row.querySelector('.track-remove-btn');
|
|
const titleInput = row.querySelector('.track-title-input');
|
|
if (removeBtn) {
|
|
removeBtn.addEventListener('click', () => {
|
|
row.remove();
|
|
recalcTrackCount();
|
|
});
|
|
}
|
|
if (titleInput) {
|
|
titleInput.addEventListener('input', recalcTrackCount);
|
|
}
|
|
};
|
|
|
|
Array.from(trackBody.querySelectorAll('tr')).forEach((row) => bindRow(row));
|
|
|
|
addBtn.addEventListener('click', () => {
|
|
const fragment = rowTemplate.content.cloneNode(true);
|
|
const row = fragment.querySelector('tr');
|
|
if (!row) {
|
|
return;
|
|
}
|
|
const currentRows = trackBody.querySelectorAll('tr').length;
|
|
const trackNumberInput = row.querySelector('input[name="track_number[]"]');
|
|
if (trackNumberInput) {
|
|
trackNumberInput.value = String(currentRows + 1);
|
|
}
|
|
bindRow(row);
|
|
trackBody.appendChild(row);
|
|
});
|
|
|
|
if (coverInput && coverPreview && coverPlaceholder) {
|
|
coverInput.addEventListener('change', () => {
|
|
const file = coverInput.files && coverInput.files.length > 0 ? coverInput.files[0] : null;
|
|
if (!file) {
|
|
coverPreview.src = '';
|
|
coverPreview.classList.add('d-none');
|
|
coverPlaceholder.classList.remove('d-none');
|
|
return;
|
|
}
|
|
|
|
const objectUrl = URL.createObjectURL(file);
|
|
coverPreview.src = objectUrl;
|
|
coverPreview.classList.remove('d-none');
|
|
coverPlaceholder.classList.add('d-none');
|
|
coverPreview.onload = () => URL.revokeObjectURL(objectUrl);
|
|
});
|
|
}
|
|
|
|
recalcTrackCount();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|