templates/pages/livreDetail.html.twig line 1

Open in your IDE?
  1. {% extends 'base.html.twig' %}
  2. {% block body %}
  3. <div class="container-fluid px-4">
  4. <!-- Bouton retour -->
  5. <div class="mb-3">
  6. <a href="javascript:history.back()" class="btn btn-outline-secondary btn-sm">
  7. <i class="fas fa-arrow-left"></i> Retour
  8. </a>
  9. </div>
  10. <!-- Card principale -->
  11. <div class="card shadow-sm">
  12. <div class="card-header" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
  13. <h4 class="mb-0 text-white">
  14. <i class="fas fa-book"></i> {{ livre.titre }}
  15. {% if livre.tome and livre.tome > 0 %}
  16. <span class="badge badge-light ml-2">Tome {{ livre.tome }}</span>
  17. {% endif %}
  18. </h4>
  19. </div>
  20. <div class="card-body">
  21. <div class="row">
  22. <!-- Colonne Image -->
  23. <div class="col-md-4 text-center mb-4">
  24. <div class="livre-cover-container">
  25. <img id='img-{{ livre.id }}'
  26. class="img-fluid rounded shadow"
  27. alt="{{ livre.titre }}"
  28. src="{{ livre.getBestImage }}"
  29. style="max-height: 400px; object-fit: contain;" />
  30. </div>
  31. {% if is_granted('IS_AUTHENTICATED_FULLY') %}
  32. <div class="mt-3">
  33. {% if livre.isbn %}
  34. <button type="button" id="scrapeCoverBtn" class="btn btn-primary btn-sm mb-2" data-livre-id="{{ livre.id }}">
  35. <i class="fas fa-magic"></i> Scraper des images
  36. </button>
  37. <br>
  38. {% endif %}
  39. {% if livre.getImage2 %}
  40. <span class="badge badge-info mb-2"><i class="fas fa-link"></i> Image externe</span>
  41. <br>
  42. <button type="button" id="removeCoverUrlBtn" class="btn btn-outline-danger btn-sm">
  43. <i class="fas fa-undo"></i> Image originale
  44. </button>
  45. {% else %}
  46. {% if livre.isbn %}
  47. <button type="button" id="searchCoverBtn" class="btn btn-outline-primary btn-sm" data-livre-id="{{ livre.id }}">
  48. <i class="fas fa-search"></i> Chercher couverture
  49. </button>
  50. {% endif %}
  51. <button type="button" id="manualUrlBtn" class="btn btn-outline-secondary btn-sm">
  52. <i class="fas fa-link"></i> URL manuelle
  53. </button>
  54. {% endif %}
  55. <div id="coverSearchResult" class="mt-2" style="display: none;">
  56. <div id="coverPreview" class="mb-2"></div>
  57. <button type="button" id="applyCoverBtn" class="btn btn-success btn-sm" style="display: none;">
  58. <i class="fas fa-check"></i> Utiliser
  59. </button>
  60. <span id="coverMessage" class="text-muted"></span>
  61. </div>
  62. <div id="manualUrlForm" class="mt-3 text-left" style="display: none;">
  63. <p class="small text-muted mb-2">
  64. Rechercher sur :
  65. {% if livre.amazon %}
  66. <a href="{{ livre.amazon }}" target="_blank" class="badge badge-warning"><i class="fas fa-external-link-alt"></i> Produit</a>
  67. {% endif %}
  68. {% if livre.isbn %}
  69. <a href="https://www.amazon.fr/s?k={{ livre.isbn }}" target="_blank" class="badge badge-secondary">Amazon</a>
  70. <a href="https://www.google.com/search?tbm=isch&q={{ livre.isbn }}+couverture" target="_blank" class="badge badge-success">Google</a>
  71. {% endif %}
  72. </p>
  73. <div class="input-group input-group-sm">
  74. <input type="url" id="manualUrlInput" class="form-control" placeholder="URL de l'image...">
  75. <div class="input-group-append">
  76. <button type="button" id="previewManualUrlBtn" class="btn btn-primary">
  77. <i class="fas fa-eye"></i>
  78. </button>
  79. </div>
  80. </div>
  81. <div id="manualPreview" class="mt-2"></div>
  82. </div>
  83. </div>
  84. {% endif %}
  85. </div>
  86. <!-- Colonne Informations -->
  87. <div class="col-md-8">
  88. <!-- Auteurs -->
  89. {% if livre.listeAuteur|length > 0 %}
  90. <div class="mb-3">
  91. <h6 class="text-muted mb-2"><i class="fas fa-pen-fancy"></i> Auteur(s)</h6>
  92. <div>
  93. {% for auteur in livre.listeAuteur %}
  94. <a href="{{ path('auteur_detail', {'id': auteur.auteur.id}) }}" class="badge badge-primary mr-1 mb-1" style="font-size: 0.9rem;">
  95. {{ auteur.auteur.nom }}
  96. </a>
  97. {% endfor %}
  98. </div>
  99. </div>
  100. {% endif %}
  101. <!-- Infos principales -->
  102. <div class="row">
  103. <div class="col-sm-6">
  104. <div class="info-item mb-3">
  105. <small class="text-muted d-block"><i class="fas fa-building"></i> Éditeur</small>
  106. <strong>{% if livre.edition %}{{ livre.edition.nom }}{% else %}-{% endif %}</strong>
  107. </div>
  108. </div>
  109. <div class="col-sm-6">
  110. <div class="info-item mb-3">
  111. <small class="text-muted d-block"><i class="fas fa-calendar"></i> Année</small>
  112. <strong>{% if livre.annee %}{{ livre.annee }}{% else %}-{% endif %}</strong>
  113. </div>
  114. </div>
  115. <div class="col-sm-6">
  116. <div class="info-item mb-3">
  117. <small class="text-muted d-block"><i class="fas fa-barcode"></i> ISBN</small>
  118. <strong>{% if livre.isbn %}{{ livre.isbn }}{% else %}-{% endif %}</strong>
  119. </div>
  120. </div>
  121. <div class="col-sm-6">
  122. <div class="info-item mb-3">
  123. <small class="text-muted d-block"><i class="fas fa-euro-sign"></i> Prix</small>
  124. <strong>{{ livre.prixBase|number_format(2) }} {% if livre.monnaie %}{{ livre.monnaie.symbole }}{% endif %}</strong>
  125. </div>
  126. </div>
  127. {% if livre.collection %}
  128. <div class="col-sm-6">
  129. <div class="info-item mb-3">
  130. <small class="text-muted d-block"><i class="fas fa-layer-group"></i> Collection</small>
  131. <strong>{{ livre.collection.nom }}</strong>
  132. </div>
  133. </div>
  134. {% endif %}
  135. {% if livre.category %}
  136. <div class="col-sm-6">
  137. <div class="info-item mb-3">
  138. <small class="text-muted d-block"><i class="fas fa-tag"></i> Catégorie</small>
  139. <strong>{{ livre.category.nom }}</strong>
  140. </div>
  141. </div>
  142. {% endif %}
  143. {% if livre.pages and livre.pages > 0 %}
  144. <div class="col-sm-6">
  145. <div class="info-item mb-3">
  146. <small class="text-muted d-block"><i class="fas fa-file-alt"></i> Pages</small>
  147. <strong>{{ livre.pages }}</strong>
  148. </div>
  149. </div>
  150. {% endif %}
  151. {% if livre.amazon %}
  152. <div class="col-sm-6">
  153. <div class="info-item mb-3">
  154. <small class="text-muted d-block"><i class="fas fa-external-link-alt"></i> Lien</small>
  155. <a href="{{ livre.amazon }}" target="_blank" class="btn btn-sm btn-outline-warning">
  156. <i class="fas fa-shopping-cart"></i> Voir le produit
  157. </a>
  158. </div>
  159. </div>
  160. {% endif %}
  161. </div>
  162. <!-- Synopsis -->
  163. {% if livre.resume %}
  164. <div class="mt-3">
  165. <h6 class="text-muted mb-2"><i class="fas fa-align-left"></i> Synopsis</h6>
  166. <div class="bg-light p-3 rounded" style="max-height: 200px; overflow-y: auto;">
  167. {{ livre.resume|nl2br }}
  168. </div>
  169. </div>
  170. {% endif %}
  171. </div>
  172. </div>
  173. </div>
  174. </div>
  175. <!-- Propriétaires -->
  176. {% if livre.listeUser|length > 0 %}
  177. <div class="card mt-4 shadow-sm">
  178. <div class="card-header bg-dark text-white">
  179. <h5 class="mb-0"><i class="fas fa-users"></i> Propriétaires ({{ livre.listeUser|length }})</h5>
  180. </div>
  181. <div class="card-body p-0">
  182. <div class="table-responsive">
  183. <table class="table table-hover mb-0">
  184. <thead class="thead-light">
  185. <tr>
  186. <th><i class="fas fa-user"></i> Utilisateur</th>
  187. <th><i class="fas fa-calendar-alt"></i> Date d'acquisition</th>
  188. <th><i class="fas fa-comment"></i> Commentaire</th>
  189. </tr>
  190. </thead>
  191. <tbody>
  192. {% for lienuserLivre in livre.listeUser %}
  193. <tr>
  194. <td>
  195. <strong>{{ lienuserLivre.user.name }} {{ lienuserLivre.user.lastName }}</strong>
  196. </td>
  197. <td>
  198. {% if lienuserLivre.dateAchat %}
  199. {{ lienuserLivre.dateAchat|date('d/m/Y') }}
  200. {% else %}
  201. <span class="text-muted">-</span>
  202. {% endif %}
  203. </td>
  204. <td>
  205. {% if lienuserLivre.commentaire %}
  206. {{ lienuserLivre.commentaire }}
  207. {% else %}
  208. <span class="text-muted">-</span>
  209. {% endif %}
  210. </td>
  211. </tr>
  212. {% endfor %}
  213. </tbody>
  214. </table>
  215. </div>
  216. </div>
  217. </div>
  218. {% endif %}
  219. </div>
  220. <!-- Modale de sélection d'images scrapées -->
  221. <div class="modal fade" id="scrapedImagesModal" tabindex="-1" role="dialog">
  222. <div class="modal-dialog modal-xl" role="document">
  223. <div class="modal-content">
  224. <div class="modal-header" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
  225. <h5 class="modal-title text-white">
  226. <i class="fas fa-images"></i> Images trouvées
  227. </h5>
  228. <button type="button" class="close text-white" data-dismiss="modal">
  229. <span>&times;</span>
  230. </button>
  231. </div>
  232. <div class="modal-body">
  233. <div id="scrapedImagesLoading" class="text-center py-5">
  234. <i class="fas fa-spinner fa-spin fa-3x text-primary"></i>
  235. <p class="mt-3">Recherche en cours sur BDGuest, Fnac, Decitre, Amazon...</p>
  236. <small class="text-muted">Cela peut prendre jusqu'à 2 minutes</small>
  237. </div>
  238. <div id="scrapedImagesContent" style="display: none;">
  239. <p class="text-muted mb-3">
  240. <i class="fas fa-info-circle"></i> Cliquez sur une image pour la sélectionner
  241. </p>
  242. <div id="scrapedImagesGrid" class="row"></div>
  243. </div>
  244. <div id="scrapedImagesError" class="alert alert-warning" style="display: none;">
  245. <i class="fas fa-exclamation-triangle"></i> <span id="scrapedImagesErrorMsg"></span>
  246. </div>
  247. </div>
  248. </div>
  249. </div>
  250. </div>
  251. <style>
  252. .info-item {
  253. padding: 10px;
  254. background: #f8f9fa;
  255. border-radius: 8px;
  256. border-left: 3px solid #667eea;
  257. }
  258. .livre-cover-container {
  259. background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  260. padding: 20px;
  261. border-radius: 10px;
  262. }
  263. .scraped-image-card {
  264. cursor: pointer;
  265. transition: all 0.3s;
  266. border: 3px solid transparent;
  267. }
  268. .scraped-image-card:hover {
  269. transform: translateY(-5px);
  270. box-shadow: 0 8px 25px rgba(0,0,0,0.2);
  271. }
  272. .scraped-image-card.selected {
  273. border-color: #667eea;
  274. box-shadow: 0 0 20px rgba(102, 126, 234, 0.5);
  275. }
  276. .scraped-image-card img {
  277. max-height: 300px;
  278. object-fit: contain;
  279. width: 100%;
  280. }
  281. </style>
  282. {% endblock %}
  283. {% block javascripts %}
  284. {{ parent() }}
  285. {% if is_granted('IS_AUTHENTICATED_FULLY') and livre.isbn %}
  286. <script>
  287. $(document).ready(function() {
  288. var foundImageUrl = null;
  289. var livreId = {{ livre.id }};
  290. $('#searchCoverBtn').on('click', function() {
  291. var btn = $(this);
  292. btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Recherche en cours...');
  293. $('#coverSearchResult').show();
  294. $('#coverMessage').text('Recherche en cours...');
  295. $('#coverPreview').empty();
  296. $('#applyCoverBtn').hide();
  297. $('#manualUrlForm').hide();
  298. $.ajax({
  299. url: '/livre/' + livreId + '/rechercher-couverture',
  300. method: 'GET',
  301. success: function(response) {
  302. btn.prop('disabled', false).html('<i class="fas fa-search"></i> Rechercher automatiquement');
  303. if (response.success && response.images && response.images.length > 0) {
  304. var html = '<p class="text-success"><i class="fas fa-check-circle"></i> ' + response.message + '</p>';
  305. html += '<div class="row">';
  306. response.images.forEach(function(img, index) {
  307. html += '<div class="col-4 col-md-3 mb-2">';
  308. html += '<div class="card h-100">';
  309. html += '<img src="' + img.url + '" class="card-img-top img-select-cover" style="height: 120px; object-fit: contain; cursor: pointer;" data-url="' + img.url + '" alt="Option ' + (index+1) + '" onerror="this.parentElement.parentElement.remove()">';
  310. html += '<div class="card-body p-1 text-center"><small class="text-muted">' + img.source + '</small></div>';
  311. html += '</div></div>';
  312. });
  313. html += '</div>';
  314. html += '<p class="small text-muted mt-2">Cliquez sur une image pour la sélectionner</p>';
  315. $('#coverPreview').html(html);
  316. } else {
  317. $('#coverMessage').html('<span class="text-warning"><i class="fas fa-exclamation-triangle"></i> ' + (response.message || 'Aucune image trouvée') + '</span>');
  318. }
  319. },
  320. error: function() {
  321. btn.prop('disabled', false).html('<i class="fas fa-search"></i> Rechercher automatiquement');
  322. $('#coverMessage').html('<span class="text-danger"><i class="fas fa-times-circle"></i> Erreur lors de la recherche</span>');
  323. }
  324. });
  325. });
  326. // Sélection d'une image dans la galerie
  327. $(document).on('click', '.img-select-cover', function() {
  328. $('.img-select-cover').removeClass('border-success').css('border-width', '1px');
  329. $(this).addClass('border-success').css('border-width', '3px');
  330. foundImageUrl = $(this).data('url');
  331. $('#applyCoverBtn').show();
  332. });
  333. $('#applyCoverBtn').on('click', function() {
  334. if (!foundImageUrl) return;
  335. var btn = $(this);
  336. btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Enregistrement...');
  337. $.ajax({
  338. url: '/livre/' + livreId + '/mettre-a-jour-couverture',
  339. method: 'POST',
  340. data: { image_url: foundImageUrl },
  341. success: function(response) {
  342. if (response.success) {
  343. $('#img-' + livreId).attr('src', response.imageUrl);
  344. $('#coverSearchResult').html('<span class="text-success"><i class="fas fa-check-circle"></i> ' + response.message + '</span>');
  345. setTimeout(function() { location.reload(); }, 1500);
  346. } else {
  347. btn.prop('disabled', false).html('<i class="fas fa-check"></i> Utiliser cette image');
  348. $('#coverMessage').html('<span class="text-danger">' + response.message + '</span>');
  349. }
  350. },
  351. error: function() {
  352. btn.prop('disabled', false).html('<i class="fas fa-check"></i> Utiliser cette image');
  353. $('#coverMessage').html('<span class="text-danger">Erreur lors de l\'enregistrement</span>');
  354. }
  355. });
  356. });
  357. $('#removeCoverUrlBtn').on('click', function() {
  358. if (!confirm('Revenir à l\'image originale stockée en base ?')) return;
  359. var btn = $(this);
  360. btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Suppression...');
  361. $.ajax({
  362. url: '/livre/' + livreId + '/supprimer-url-couverture',
  363. method: 'POST',
  364. success: function(response) {
  365. if (response.success) {
  366. location.reload();
  367. } else {
  368. btn.prop('disabled', false).html('<i class="fas fa-undo"></i> Revenir à l\'image originale');
  369. alert(response.message);
  370. }
  371. },
  372. error: function() {
  373. btn.prop('disabled', false).html('<i class="fas fa-undo"></i> Revenir à l\'image originale');
  374. alert('Erreur lors de la suppression');
  375. }
  376. });
  377. });
  378. // Saisie manuelle d'URL
  379. $('#manualUrlBtn').on('click', function() {
  380. $('#manualUrlForm').toggle();
  381. $('#coverSearchResult').hide();
  382. });
  383. $('#previewManualUrlBtn').on('click', function() {
  384. var url = $('#manualUrlInput').val().trim();
  385. if (!url) {
  386. alert('Veuillez saisir une URL');
  387. return;
  388. }
  389. foundImageUrl = url;
  390. $('#manualPreview').html(
  391. '<img src="' + url + '" class="img-thumbnail" style="max-height: 200px;" alt="Prévisualisation" onerror="this.parentElement.innerHTML=\'<span class=text-danger>Image non accessible</span>\'">' +
  392. '<br><button type="button" id="applyManualUrlBtn" class="btn btn-success btn-sm mt-2"><i class="fas fa-check"></i> Utiliser cette image</button>'
  393. );
  394. });
  395. $(document).on('click', '#applyManualUrlBtn', function() {
  396. if (!foundImageUrl) return;
  397. var btn = $(this);
  398. btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Enregistrement...');
  399. $.ajax({
  400. url: '/livre/' + livreId + '/mettre-a-jour-couverture',
  401. method: 'POST',
  402. data: { image_url: foundImageUrl },
  403. success: function(response) {
  404. if (response.success) {
  405. location.reload();
  406. } else {
  407. btn.prop('disabled', false).html('<i class="fas fa-check"></i> Utiliser cette image');
  408. alert(response.message);
  409. }
  410. },
  411. error: function() {
  412. btn.prop('disabled', false).html('<i class="fas fa-check"></i> Utiliser cette image');
  413. alert('Erreur lors de l\'enregistrement');
  414. }
  415. });
  416. });
  417. // Scraper des images via Puppeteer
  418. $('#scrapeCoverBtn').on('click', function() {
  419. var livreId = $(this).data('livre-id');
  420. // Ouvrir la modale
  421. $('#scrapedImagesModal').modal('show');
  422. // Réinitialiser l'état
  423. $('#scrapedImagesLoading').show();
  424. $('#scrapedImagesContent').hide();
  425. $('#scrapedImagesError').hide();
  426. $('#scrapedImagesGrid').empty();
  427. // Appeler le contrôleur qui va appeler Puppeteer
  428. $.ajax({
  429. url: '/livre/' + livreId + '/scrape-covers',
  430. method: 'GET',
  431. dataType: 'json',
  432. timeout: 120000, // 2 minutes
  433. success: function(response) {
  434. $('#scrapedImagesLoading').hide();
  435. if (response.images && response.images.length > 0) {
  436. $('#scrapedImagesContent').show();
  437. // Afficher les images
  438. response.images.forEach(function(image) {
  439. var card = $('<div class="col-md-4 mb-3">' +
  440. '<div class="card scraped-image-card h-100" data-image-url="' + image.url + '">' +
  441. '<div class="card-body text-center p-2">' +
  442. '<img src="' + image.url + '" alt="Couverture" class="img-fluid mb-2" style="max-height: 300px; object-fit: contain;">' +
  443. '<div class="mb-2">' +
  444. '<span class="badge badge-primary">' + image.source + '</span>' +
  445. '<span class="badge badge-secondary ml-1">' + image.quality + '</span>' +
  446. '</div>' +
  447. '<button class="btn btn-success btn-sm btn-block select-image-btn">Utiliser</button>' +
  448. '</div>' +
  449. '</div>' +
  450. '</div>');
  451. $('#scrapedImagesGrid').append(card);
  452. });
  453. // Gérer la sélection d'image
  454. $('.select-image-btn').on('click', function() {
  455. var imageUrl = $(this).closest('.scraped-image-card').data('image-url');
  456. if (!confirm('Utiliser cette image comme couverture ?')) {
  457. return;
  458. }
  459. // Marquer comme sélectionnée
  460. $('.scraped-image-card').removeClass('selected');
  461. $(this).closest('.scraped-image-card').addClass('selected');
  462. $(this).html('<i class="fas fa-spinner fa-spin"></i>').prop('disabled', true);
  463. // Enregistrer l'URL
  464. $.ajax({
  465. url: '/livre/' + livreId + '/update-cover-url',
  466. method: 'POST',
  467. data: { imageUrl: imageUrl },
  468. dataType: 'json',
  469. success: function(response) {
  470. if (response.success) {
  471. $('#scrapedImagesModal').modal('hide');
  472. location.reload();
  473. } else {
  474. alert('Erreur: ' + (response.error || 'Erreur inconnue'));
  475. $('.select-image-btn').html('Utiliser').prop('disabled', false);
  476. }
  477. },
  478. error: function(xhr) {
  479. var errorMsg = 'Erreur lors de l\'enregistrement de l\'image';
  480. if (xhr.responseJSON && xhr.responseJSON.error) {
  481. errorMsg = xhr.responseJSON.error;
  482. }
  483. alert(errorMsg);
  484. $('.select-image-btn').html('Utiliser').prop('disabled', false);
  485. }
  486. });
  487. });
  488. } else {
  489. $('#scrapedImagesError').show();
  490. $('#scrapedImagesErrorMsg').text('Aucune image trouvée sur BDGuest, Fnac, Decitre et Amazon pour l\'ISBN ' + response.isbn);
  491. }
  492. },
  493. error: function(xhr) {
  494. $('#scrapedImagesLoading').hide();
  495. $('#scrapedImagesError').show();
  496. var errorMsg = 'Erreur lors de la recherche d\'images. Le service Puppeteer est-il démarré ?';
  497. if (xhr.responseJSON && xhr.responseJSON.error) {
  498. errorMsg = xhr.responseJSON.error;
  499. }
  500. $('#scrapedImagesErrorMsg').text(errorMsg);
  501. }
  502. });
  503. });
  504. });
  505. </script>
  506. {% endif %}
  507. {% endblock %}