From 17c1f6eb5e9508872e297ac30313b055fc4d9146 Mon Sep 17 00:00:00 2001 From: Thomas Sevagen Date: Wed, 5 Mar 2025 15:29:02 +0100 Subject: [PATCH] Mag 212 (#388) * refactor(notify): migrate notification templates and configurations to view-src * feat(slide): add SlideController, SlideService, and DefaultSlideService classes * feat(export): add ExportController and DefaultExportService for PPTX export functionality * fix(export): update magnet number handling based on layout type in DefaultExportService * feat(slide): implement Slide classes and update slide object handling in DefaultExportService * feat(slide): enhance slide model with new SlideDescription and SlideTitle classes; update SlideText and SlideMedia constructors * feat(export): enhance export functionality by incorporating user information and refactoring slide creation logic * feat(export): update export functionality to return XMLSlideShow and improve slide creation logic * feat(slide): update SlideHelper and SlideText no double text and no corrupt * feat(slide): simplify SlideText createApacheSlide method by removing unnecessary comments and improving HTML parsing * feat(slide): add createImage method to SlideHelper and extend SlideProperties with file extension * feat(slide): refactor image stable * feat(export): add caption to resource data in DefaultExportService * feat(slide): update SlideMedia to include caption and version bump to 2.2-SNAPSHOT * feat(notify): remove notify_board.json configuration file * feat(slide) : #MAG-543 add first slide (#390) --------- Co-authored-by: Florent Mariotti * feat(slideMedia): #MAG-548 slide media integration (#391) * feat(media): add audio icon SVG and enhance SlideMedia with media type detection feat(export): enhance media handling by copying media parts and relationships during slide export media audio stable media based on content-type feat(slide): refactor media icon creation and positioning for improved layout video stable * feat(export): add CONTENTTYPE constant and refactor DefaultExportService to use it * feat(export): refactor DefaultExportService to use constants for document fields * feat(slideshow): add standard icon and video dimensions as constants * refactor(export): remove debug print statements from ExportController * refactor(export): remove unused imports from ExportController * feat(slideshow) : #MAG-545 add sections slides (#392) --------- Co-authored-by: Florent Mariotti * feat(slideshow) : #MAG-544 add description slide (#393) Co-authored-by: Florent Mariotti * fix(slide) : #MAG-545 fix description and title flow (#394) --------- Co-authored-by: Florent Mariotti * feat(slides) : #MAG-547 add board slides (#395) --------- Co-authored-by: Thomas Sevagen Co-authored-by: Florent Mariotti * feat(slides) : #MAG-546 add link slides (#396) Co-authored-by: Florent Mariotti * feat(slides) : #MAG-551 add file slides (#397) --------- Co-authored-by: Florent Mariotti * feat(slides) : add notes to link, board and media slides * feat(slides) : #MAG-554 add archive with slideshow and its documents (#398) Co-authored-by: Florent Mariotti * feat(slides) : #MAG-555 add front modal and button for export (#399) * feat(slides) : #MAG-555 add front modal and button for export --------- Co-authored-by: Florent Mariotti * Mag 212fixes (#400) --------- Co-authored-by: Florent Mariotti --------- Co-authored-by: Florent Mariotti Co-authored-by: Florent Mariotti --- backend/pom.xml | 31 +- .../src/main/java/fr/cgi/magneto/Magneto.java | 1 + .../magneto/controller/ExportController.java | 39 ++ .../core/constants/CollectionsConstant.java | 6 + .../fr/cgi/magneto/core/constants/Field.java | 16 +- .../magneto/core/constants/MagnetoPaths.java | 6 + .../cgi/magneto/core/constants/Slideshow.java | 223 ++++++ .../magneto/core/enums/FileFormatManager.java | 121 ++++ .../magneto/core/enums/SlideResourceType.java | 37 + .../fr/cgi/magneto/factory/SlideFactory.java | 50 ++ .../fr/cgi/magneto/helper/SlideHelper.java | 649 ++++++++++++++++++ .../java/fr/cgi/magneto/model/Section.java | 15 + .../model/properties/SlideProperties.java | 261 +++++++ .../fr/cgi/magneto/model/slides/Slide.java | 10 + .../cgi/magneto/model/slides/SlideBoard.java | 54 ++ .../model/slides/SlideDescription.java | 35 + .../cgi/magneto/model/slides/SlideFile.java | 48 ++ .../cgi/magneto/model/slides/SlideLink.java | 37 + .../cgi/magneto/model/slides/SlideMedia.java | 66 ++ .../cgi/magneto/model/slides/SlideText.java | 173 +++++ .../cgi/magneto/model/slides/SlideTitle.java | 55 ++ .../fr/cgi/magneto/service/CardService.java | 10 + .../fr/cgi/magneto/service/ExportService.java | 18 + .../cgi/magneto/service/SectionService.java | 5 +- .../cgi/magneto/service/ServiceFactory.java | 7 + .../service/impl/DefaultCardService.java | 16 + .../service/impl/DefaultExportService.java | 595 ++++++++++++++++ .../service/impl/DefaultSectionService.java | 25 + backend/src/main/resources/i18n/de.json | 6 + backend/src/main/resources/i18n/en.json | 6 + backend/src/main/resources/i18n/es.json | 6 + backend/src/main/resources/i18n/fr.json | 14 + backend/src/main/resources/i18n/it.json | 6 + backend/src/main/resources/i18n/pt.json | 6 + backend/src/main/resources/img/audio_icon.svg | 6 + .../main/resources/img/extension/audio.svg | 100 +++ .../main/resources/img/extension/default.svg | 110 +++ .../src/main/resources/img/extension/file.svg | 100 +++ .../main/resources/img/extension/image.svg | 109 +++ .../src/main/resources/img/extension/link.svg | 108 +++ .../src/main/resources/img/extension/pdf.svg | 99 +++ .../main/resources/img/extension/sheet.svg | 120 ++++ .../src/main/resources/img/extension/text.svg | 123 ++++ .../main/resources/img/extension/video.svg | 113 +++ .../components/export-modal/ExportModal.tsx | 195 ++++++ frontend/src/components/export-modal/style.ts | 74 ++ frontend/src/components/export-modal/types.ts | 4 + .../src/components/read-view/ReadView.tsx | 2 +- .../toaster-container/ToasterContainer.tsx | 17 + frontend/src/services/api/export.service.ts | 29 + 50 files changed, 3953 insertions(+), 9 deletions(-) create mode 100644 backend/src/main/java/fr/cgi/magneto/controller/ExportController.java create mode 100644 backend/src/main/java/fr/cgi/magneto/core/constants/MagnetoPaths.java create mode 100644 backend/src/main/java/fr/cgi/magneto/core/constants/Slideshow.java create mode 100644 backend/src/main/java/fr/cgi/magneto/core/enums/FileFormatManager.java create mode 100644 backend/src/main/java/fr/cgi/magneto/core/enums/SlideResourceType.java create mode 100644 backend/src/main/java/fr/cgi/magneto/factory/SlideFactory.java create mode 100644 backend/src/main/java/fr/cgi/magneto/helper/SlideHelper.java create mode 100644 backend/src/main/java/fr/cgi/magneto/model/properties/SlideProperties.java create mode 100644 backend/src/main/java/fr/cgi/magneto/model/slides/Slide.java create mode 100644 backend/src/main/java/fr/cgi/magneto/model/slides/SlideBoard.java create mode 100644 backend/src/main/java/fr/cgi/magneto/model/slides/SlideDescription.java create mode 100644 backend/src/main/java/fr/cgi/magneto/model/slides/SlideFile.java create mode 100644 backend/src/main/java/fr/cgi/magneto/model/slides/SlideLink.java create mode 100644 backend/src/main/java/fr/cgi/magneto/model/slides/SlideMedia.java create mode 100644 backend/src/main/java/fr/cgi/magneto/model/slides/SlideText.java create mode 100644 backend/src/main/java/fr/cgi/magneto/model/slides/SlideTitle.java create mode 100644 backend/src/main/java/fr/cgi/magneto/service/ExportService.java create mode 100644 backend/src/main/java/fr/cgi/magneto/service/impl/DefaultExportService.java create mode 100644 backend/src/main/resources/img/audio_icon.svg create mode 100644 backend/src/main/resources/img/extension/audio.svg create mode 100644 backend/src/main/resources/img/extension/default.svg create mode 100644 backend/src/main/resources/img/extension/file.svg create mode 100644 backend/src/main/resources/img/extension/image.svg create mode 100644 backend/src/main/resources/img/extension/link.svg create mode 100644 backend/src/main/resources/img/extension/pdf.svg create mode 100644 backend/src/main/resources/img/extension/sheet.svg create mode 100644 backend/src/main/resources/img/extension/text.svg create mode 100644 backend/src/main/resources/img/extension/video.svg create mode 100644 frontend/src/components/export-modal/ExportModal.tsx create mode 100644 frontend/src/components/export-modal/style.ts create mode 100644 frontend/src/components/export-modal/types.ts create mode 100644 frontend/src/services/api/export.service.ts diff --git a/backend/pom.xml b/backend/pom.xml index 690751235..f5baba6cb 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -1,5 +1,6 @@ - 4.0.0 @@ -37,6 +38,9 @@ 6.4.0 0.10.2 2.2.2 + 5.2.3 + 1.18.3 + 0.4.20 @@ -117,5 +121,30 @@ ${toolsVersion} test + + org.apache.poi + poi + ${apachePoiVersion} + + + org.apache.poi + poi-ooxml + ${apachePoiVersion} + + + org.apache.poi + poi-ooxml-full + ${apachePoiVersion} + + + org.jsoup + jsoup + ${jsoupVersion} + + + net.coobird + thumbnailator + ${thumbnailatorVersion} + diff --git a/backend/src/main/java/fr/cgi/magneto/Magneto.java b/backend/src/main/java/fr/cgi/magneto/Magneto.java index b4b17bccc..44ad24fa1 100644 --- a/backend/src/main/java/fr/cgi/magneto/Magneto.java +++ b/backend/src/main/java/fr/cgi/magneto/Magneto.java @@ -50,6 +50,7 @@ public void start(Promise startPromise) throws Exception { addController(new CommentController(serviceFactory)); addController(new BoardAccessController(serviceFactory)); addController(new WorkspaceController(serviceFactory)); + addController(new ExportController(serviceFactory)); final EventBus eb = getEventBus(vertx); diff --git a/backend/src/main/java/fr/cgi/magneto/controller/ExportController.java b/backend/src/main/java/fr/cgi/magneto/controller/ExportController.java new file mode 100644 index 000000000..0b26a53ed --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/controller/ExportController.java @@ -0,0 +1,39 @@ +package fr.cgi.magneto.controller; + +import fr.cgi.magneto.core.constants.Field; +import fr.cgi.magneto.helper.I18nHelper; +import fr.cgi.magneto.service.ExportService; +import fr.cgi.magneto.service.ServiceFactory; +import fr.wseduc.rs.ApiDoc; +import fr.wseduc.rs.Get; +import fr.wseduc.webutils.I18n; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerRequest; +import org.entcore.common.controller.ControllerHelper; +import org.entcore.common.user.UserUtils; + +public class ExportController extends ControllerHelper { + private final ExportService exportService; + + public ExportController(ServiceFactory serviceFactory) { + this.exportService = serviceFactory.exportService(); + } + + @Get("/export/slide/:boardId") + @ApiDoc("Export board to PPTX") + public void exportBoardToPPTX(HttpServerRequest request) { + String boardId = request.getParam(Field.BOARDID); + UserUtils.getUserInfos(eb, request, user -> { + I18nHelper i18nHelper = new I18nHelper(getHost(request), I18n.acceptLanguage(request)); + exportService.exportBoardToArchive(boardId, user, i18nHelper) + .onFailure(err -> renderError(request)) + .onSuccess(zip -> { + request.response() + .putHeader("Content-Type", "application/zip") + .putHeader("Content-Disposition", "attachment; filename=\"board.zip\""); + + request.response().end(Buffer.buffer(zip.toByteArray())); + }); + }); + } +} \ No newline at end of file diff --git a/backend/src/main/java/fr/cgi/magneto/core/constants/CollectionsConstant.java b/backend/src/main/java/fr/cgi/magneto/core/constants/CollectionsConstant.java index 7c0d4ab18..7d585a4f6 100644 --- a/backend/src/main/java/fr/cgi/magneto/core/constants/CollectionsConstant.java +++ b/backend/src/main/java/fr/cgi/magneto/core/constants/CollectionsConstant.java @@ -6,6 +6,12 @@ public class CollectionsConstant { public static final String CARD_COLLECTION = "magneto.cards"; public static final String SECTION_COLLECTION = "magneto.sections"; public static final String BOARD_VIEW_COLLECTION = "magneto.boards.access"; + public static final String I18N_SLIDESHOW_OWNER = "magneto.slideshow.owner"; + public static final String I18N_SLIDESHOW_UPDATED = "magneto.slideshow.updated.the"; + public static final String I18N_SLIDESHOW_MAGNETS = "magneto.slideshow.magnets"; + public static final String I18N_SLIDESHOW_SHARED = "magneto.slideshow.shared"; + public static final String I18N_SLIDESHOW_PLATFORM = "magneto.slideshow.platform"; + public static final String I18N_SLIDESHOW_FILENAME = "magneto.slideshow.filename"; public static final String WORKSPACE_DOCUMENTS = "documents"; } diff --git a/backend/src/main/java/fr/cgi/magneto/core/constants/Field.java b/backend/src/main/java/fr/cgi/magneto/core/constants/Field.java index e4659d0e6..630f6cbf5 100644 --- a/backend/src/main/java/fr/cgi/magneto/core/constants/Field.java +++ b/backend/src/main/java/fr/cgi/magneto/core/constants/Field.java @@ -89,7 +89,6 @@ public class Field { public static final String CURSOR = "cursor"; public static final String FIRSTBATCH = "firstBatch"; - // CARD FIELD public static final String BOARDID = "boardId"; @@ -102,6 +101,7 @@ public class Field { public static final String RESOURCE_MAGNET = "magnet"; public static final String RESOURCE_SECTION = "section"; public static final String RESOURCEURL = "resourceUrl"; + public static final String MAGNET_NUMBER = "magnetNumber"; public static final String LASTMODIFIERID = "lastModifierId"; public static final String LASTMODIFIERNAME = "lastModifierName"; public static final String LASTCOMMENT = "lastComment"; @@ -124,7 +124,6 @@ public class Field { public static final String NUMBER = "number"; - // METADATA FIELD public static final String NAME = "name"; @@ -134,6 +133,7 @@ public class Field { public static final String CHARSET = "charset"; public static final String SIZE = "size"; public static final String METADATA = "metadata"; + public static final String CONTENTTYPE = "contentType"; public static final String INDEX = "index"; public static final String PROFILURI = "profilUri"; @@ -187,7 +187,7 @@ public class Field { public static final String BOARDURL = "boardUrl"; - //Import Export + // Import Export public static final String RAPPORT = "rapport"; public static final String RESSOURCE_NUMBER = "resourcesNumber"; @@ -210,7 +210,7 @@ public class Field { public static final String USER_2 = "User"; public static final String USERIDS = "userIds"; - //FAVORITE + // FAVORITE public static final String FAVORITE = "favorite"; public static final String FAVORITE_LIST = "favoriteList"; public static final String ISFAVORITE = "isFavorite"; @@ -230,8 +230,14 @@ public class Field { public static final String GROUP_TYPE = "groupType"; public static final String GROUP = "Group"; public static final String MEMBERS = "members"; - //notification Folder + // notification Folder public static final String FOLDERTITLE = "folderTitle"; public static final String FOLDERURL = "folderUrl"; public static final String BOARDNAME = "boardName"; + + // EXPORT SLIDE + public static final String SLIDE_OBJECTS = "slideObjects"; + public static final String BOARD_IMAGE_ID = "boardImageId"; + public static final String BUFFER = "buffer"; + public static final String FILE = "file"; } diff --git a/backend/src/main/java/fr/cgi/magneto/core/constants/MagnetoPaths.java b/backend/src/main/java/fr/cgi/magneto/core/constants/MagnetoPaths.java new file mode 100644 index 000000000..cee46195f --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/core/constants/MagnetoPaths.java @@ -0,0 +1,6 @@ +package fr.cgi.magneto.core.constants; + +public class MagnetoPaths { + public static final String MAGNETO_BOARD = "magneto#/board/"; + public static final String VIEW = "/view"; +} diff --git a/backend/src/main/java/fr/cgi/magneto/core/constants/Slideshow.java b/backend/src/main/java/fr/cgi/magneto/core/constants/Slideshow.java new file mode 100644 index 000000000..8ba56cde6 --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/core/constants/Slideshow.java @@ -0,0 +1,223 @@ +package fr.cgi.magneto.core.constants; + +import java.awt.*; + +public class Slideshow { + // Constantes de mise en page générales + public static final int MARGIN_LEFT = 80; + public static final int MARGIN_TOP_TITLE = 40; + public static final int WIDTH = 1120; + public static final String DEFAULT_FONT = "Roboto"; + public static final int SLIDE_HEIGHT = 720; + public static final int SLIDE_WIDTH = 1280; + + // Constantes pour les titres + public static final int TITLE_HEIGHT = 70; + public static final Double TITLE_FONT_SIZE = 44.0; + public static final int DESCRIPTION_TITLE_HEIGHT = 75; + public static final Double DESCRIPTION_TITLE_FONT_SIZE = 55.0; + public static final int MAIN_TITLE_HEIGHT = 100; + public static final Double MAIN_TITLE_FONT_SIZE = 100.0; + + // Constantes pour les légendes + public static final int LEGEND_HEIGHT = 70; + public static final int LEGEND_MARGIN_BOTTOM = 20; + public static final Double LEGEND_FONT_SIZE = 16.0; + public static final String LEGEND_FONT_FAMILY = "Roboto"; + + // Constantes pour le contenu + public static final int CONTENT_HEIGHT = 520; + public static final int CONTENT_MARGIN_TOP = 140; + public static final int MAIN_CONTENT_MARGIN_TOP = 300; + public static final Double CONTENT_FONT_SIZE = 36.0; + public static final int SLIDE_BOARD_CONTENT_MARGIN_TOP = 450; + public static final Double DESCRIPTION_FONT_SIZE = 24.0; + public static final int BOARD_TEXT_WIDTH = 500; + + // Constantes pour les images + public static final int MAIN_IMAGE_CONTENT_HEIGHT = 400; + public static final int IMAGE_CONTENT_HEIGHT = 480; + public static final int BOARD_IMAGE_CONTENT_HEIGHT = 250; + public static final int SVG_CONTENT_HEIGHT = 250; + public static final int SVG_CONTENT_WIDTH = 175; + + // Constantes content_type prefixes + public static final String CONTENT_TYPE_AUDIO = "audio/"; + public static final String CONTENT_TYPE_VIDEO = "video/"; + + // Constantes content_type audio + public static final String CONTENT_TYPE_AUDIO_MPEG = "audio/mpeg"; + public static final String CONTENT_TYPE_AUDIO_MP3 = "audio/mp3"; + public static final String CONTENT_TYPE_AUDIO_WAV = "audio/wav"; + public static final String CONTENT_TYPE_AUDIO_X_WAV = "audio/x-wav"; + public static final String CONTENT_TYPE_AUDIO_MP4 = "audio/mp4"; + public static final String CONTENT_TYPE_AUDIO_X_M4A = "audio/x-m4a"; + public static final String CONTENT_TYPE_AUDIO_OGG = "audio/ogg"; + + // Constantes content_type vidéo + public static final String CONTENT_TYPE_VIDEO_MP4 = "video/mp4"; + public static final String CONTENT_TYPE_VIDEO_MPEG = "video/mpeg"; + public static final String CONTENT_TYPE_VIDEO_X_MS_WMV = "video/x-ms-wmv"; + public static final String CONTENT_TYPE_VIDEO_QUICKTIME = "video/quicktime"; + public static final String CONTENT_TYPE_VIDEO_X_MATROSKA = "video/x-matroska"; + public static final String CONTENT_TYPE_VIDEO_WEBM = "video/webm"; + public static final String CONTENT_TYPE_VIDEO_X_FLV = "video/x-flv"; + public static final String CONTENT_TYPE_VIDEO_3GPP = "video/3gpp"; + public static final String CONTENT_TYPE_VIDEO_AVI = "video/avi"; + public static final String CONTENT_TYPE_VIDEO_X_MSVIDEO = "video/x-msvideo"; + + // Constantes content_type image + public static final String CONTENT_TYPE_IMAGE_JPEG = "image/jpeg"; + public static final String CONTENT_TYPE_IMAGE_JPG = "image/jpg"; + public static final String CONTENT_TYPE_IMAGE_PNG = "image/png"; + public static final String CONTENT_TYPE_IMAGE_GIF = "image/gif"; + public static final String CONTENT_TYPE_IMAGE_TIFF = "image/tiff"; + public static final String CONTENT_TYPE_IMAGE_X_EMF = "image/x-emf"; + public static final String CONTENT_TYPE_IMAGE_X_WMF = "image/x-wmf"; + public static final String CONTENT_TYPE_IMAGE_X_PICT = "image/x-pict"; + public static final String CONTENT_TYPE_IMAGE_DIB = "image/dib"; + public static final String CONTENT_TYPE_IMAGE_X_EPS = "image/x-eps"; + public static final String CONTENT_TYPE_IMAGE_X_MS_BMP = "image/x-ms-bmp"; + public static final String CONTENT_TYPE_IMAGE_BMP = "image/bmp"; + public static final String CONTENT_TYPE_IMAGE_X_WPG = "image/x-wpg"; + public static final String CONTENT_TYPE_IMAGE_VND_MS_PHOTO = "image/vnd.ms-photo"; + public static final String CONTENT_TYPE_IMAGE_SVG_XML = "image/svg+xml"; + public static final String CONTENT_TYPE_IMAGE_PREFIX = "image/"; + + // Constantes pour le formatage de texte + public static final Double H1_FONT_SIZE = 20.0; + public static final Double H2_FONT_SIZE = 18.0; + public static final Double H3_FONT_SIZE = 16.0; + public static final int H1_INDENT_LEVEL = 0; + public static final int H2_INDENT_LEVEL = 1; + public static final int H3_INDENT_LEVEL = 2; + public static final int LIST_INDENT_LEVEL = 1; + + // Constantes pour l'espacement des paragraphes + public static final Double PARAGRAPH_SPACE_BEFORE = 10.0; + public static final Double PARAGRAPH_SPACE_AFTER = 10.0; + + // Constantes pour les styles CSS + public static final String CSS_STYLE = "style"; + public static final String CSS_COLOR = "color"; + public static final String CSS_FONT_SIZE = "font-size"; + public static final String CSS_TEXT_DECORATION = "text-decoration"; + public static final String CSS_FONT_WEIGHT = "font-weight"; + public static final String CSS_FONT_STYLE = "font-style"; + + // Constantes pour les valeurs de style + public static final String VALUE_UNDERLINE = "underline"; + public static final String VALUE_BOLD = "bold"; + public static final String VALUE_BOLD_WEIGHT = "700"; + public static final String VALUE_ITALIC = "italic"; + + // Constantes pour les préfixes de noms de fichiers médias + public static final String MEDIA_TYPE_AUDIO = "audio"; + public static final String MEDIA_TYPE_VIDEO = "video"; + + // Constantes pour les chemins et relations + public static final String MEDIA_PATH_PREFIX = "/ppt/media/"; + public static final String RELATIONSHIP_MEDIA = "http://schemas.microsoft.com/office/2007/relationships/media"; + public static final String RELATIONSHIP_AUDIO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio"; + public static final String RELATIONSHIP_VIDEO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video"; + public static final String ACTION_MEDIA = "ppaction://media"; + public static final String EXTENSION_URI_MEDIA = "{DAA4B4D4-6D71-4841-9C94-3DE7FCFB9230}"; + public static final String NAMESPACE_POWERPOINT_2010 = "http://schemas.microsoft.com/office/powerpoint/2010/main"; + + // Constantes pour les paramètres de timing + public static final int TIMING_ROOT_ID = 1; + public static final int TIMING_MEDIA_ID = 2; + public static final int MEDIA_VOLUME = 80000; + + // Constantes pour les balises HTML + public static final String TAG_H1 = "h1"; + public static final String TAG_H2 = "h2"; + public static final String TAG_H3 = "h3"; + public static final String TAG_BOLD = "b"; + public static final String TAG_STRONG = "strong"; + public static final String TAG_ITALIC = "i"; + public static final String TAG_EM = "em"; + public static final String TAG_UNDERLINE = "u"; + public static final String TAG_PARAGRAPH = "p"; + public static final String TAG_UNORDERED_LIST = "ul"; + public static final String TAG_ORDERED_LIST = "ol"; + public static final String TAG_LIST_ITEM = "li"; + + // Constantes pour les extensions de fichiers + public static final String EXT_MP3 = "mp3"; + public static final String EXT_WAV = "wav"; + public static final String EXT_M4A = "m4a"; + public static final String EXT_OGG = "ogg"; + public static final String EXT_MP4 = "mp4"; + public static final String EXT_MPG = "mpg"; + public static final String EXT_WMV = "wmv"; + public static final String EXT_MOV = "mov"; + public static final String EXT_MKV = "mkv"; + public static final String EXT_WEBM = "webm"; + public static final String EXT_FLV = "flv"; + public static final String EXT_3GP = "3gp"; + public static final String EXT_AVI = "avi"; + + // Constantes pour les valeurs par défaut + public static final String DEFAULT_VIDEO_EXTENSION = EXT_MP4; + public static final String DEFAULT_AUDIO_EXTENSION = EXT_MP3; + + // Constantes pour les vignettes par défaut + public static final int THUMBNAIL_WIDTH = 320; + public static final int THUMBNAIL_HEIGHT = 180; + public static final String THUMBNAIL_FORMAT = "png"; + public static final String THUMBNAIL_FONT = "Arial"; + public static final int THUMBNAIL_FONT_STYLE = java.awt.Font.BOLD; + public static final int THUMBNAIL_FONT_SIZE = 18; + public static final String THUMBNAIL_DEFAULT_TEXT = "Video Preview"; + public static final int THUMBNAIL_TEXT_X = 100; + public static final int THUMBNAIL_TEXT_Y = 90; + public static final int VIDEO_THUMBNAIL_WIDTH = 640; + public static final int VIDEO_THUMBNAIL_HEIGHT = 360; + public static final String VIDEO_THUMBNAIL_FORMAT = "png"; + public static final String VIDEO_THUMBNAIL_FONT = "Arial"; + public static final int VIDEO_THUMBNAIL_FONT_STYLE = java.awt.Font.BOLD; + public static final int VIDEO_THUMBNAIL_FONT_SIZE = 24; + public static final String VIDEO_THUMBNAIL_TEXT = "VIDÉO"; + public static final int VIDEO_PLAY_BUTTON_RADIUS = 50; + public static final int VIDEO_TEXT_Y_OFFSET = 80; + public static final int VIDEO_PLAY_TRIANGLE_OFFSET_X = 15; + public static final int VIDEO_PLAY_TRIANGLE_OFFSET_Y = 25; + public static final int VIDEO_PLAY_TRIANGLE_OFFSET_X2 = 25; + + // Constantes pour les couleurs + public static final Color VIDEO_BACKGROUND_COLOR = new Color(20, 20, 50); + public static final Color VIDEO_PLAY_BUTTON_COLOR = new Color(255, 255, 255, 180); + public static final Color VIDEO_TEXT_COLOR = Color.WHITE; + + // Constantes pour les dimensions d'icônes standard + public static final int ICON_WIDTH = 150; + public static final int ICON_HEIGHT = 150; + + // Constantes pour les dimensions vidéo standard (16:9) + public static final int VIDEO_DISPLAY_WIDTH = 640; + public static final int VIDEO_DISPLAY_HEIGHT = 360; + + // Constantes pour l'icône audio + public static final int AUDIO_ICON_WIDTH = 100; + public static final int AUDIO_ICON_HEIGHT = 100; + public static final int AUDIO_ICON_CIRCLE_X = 5; + public static final int AUDIO_ICON_CIRCLE_Y = 5; + public static final int AUDIO_ICON_CIRCLE_WIDTH = 90; + public static final int AUDIO_ICON_CIRCLE_HEIGHT = 90; + public static final String AUDIO_ICON_FORMAT = "png"; + public static final Color AUDIO_ICON_BACKGROUND_COLOR = new Color(0, 120, 215, 240); + public static final Color AUDIO_ICON_PLAY_COLOR = Color.WHITE; + public static final int[] AUDIO_ICON_TRIANGLE_X = { 35, 70, 35 }; + public static final int[] AUDIO_ICON_TRIANGLE_Y = { 30, 50, 70 }; + // Constantes pour l'image de secours (bytecode PNG vide 1x1) + public static final byte[] FALLBACK_PNG = new byte[] { + (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, + 0x02, 0x00, 0x00, 0x00, (byte) 0x90, 0x77, 0x53, (byte) 0xDE, 0x00, 0x00, 0x00, + 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, (byte) 0xD7, 0x63, (byte) 0xF8, (byte) 0xCF, + (byte) 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, (byte) 0x18, (byte) 0xDD, (byte) 0x8D, + (byte) 0xB0, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, (byte) 0xAE, 0x42, + 0x60, (byte) 0x82 + }; +} \ No newline at end of file diff --git a/backend/src/main/java/fr/cgi/magneto/core/enums/FileFormatManager.java b/backend/src/main/java/fr/cgi/magneto/core/enums/FileFormatManager.java new file mode 100644 index 000000000..fa2b7bd01 --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/core/enums/FileFormatManager.java @@ -0,0 +1,121 @@ +package fr.cgi.magneto.core.enums; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +public class FileFormatManager { + + /** + * Obtient le type de format pour une extension donnée + */ + public static FileFormat getFormatFromExtension(String extension) { + return FileFormat.fromExtension(extension); + } + + /** + * Vérifie si une extension appartient à un format spécifique + */ + public static boolean isExtensionOfFormat(FileFormat format, String extension) { + return format != null && format.hasExtension(extension); + } + + /** + * Charge une ressource appropriée en fonction de l'extension fournie + * + * @param extension l'extension de fichier pour déterminer la ressource à charger + * @return le contenu de la ressource sous forme de byte[] + * @throws IOException si une erreur se produit lors du chargement + */ + public static String loadResourceForExtension(String extension) throws IOException { + if (extension == null || extension.isEmpty()) { + throw new IllegalArgumentException("L'extension ne peut pas être null ou vide"); + } + + // Déterminer le format à partir de l'extension + FileFormat format = FileFormat.fromExtension(extension); + + // Déterminer le chemin de la ressource en fonction du format + String resourcePath; + switch (format) { + case IMAGE: + resourcePath = "img/extension/image.svg"; + break; + case VIDEO: + resourcePath = "img/extension/video.svg"; + break; + case AUDIO: + resourcePath = "img/extension/audio.svg"; + break; + case SHEET: + resourcePath = "img/extension/sheet.svg"; + break; + case PDF: + resourcePath = "img/extension/pdf.svg"; + break; + case TEXT: + default: + resourcePath = "img/extension/default.svg"; + break; + } + + return resourcePath; + } + + /** + * Enum représentant les différents formats de fichiers + * avec leurs extensions associées + */ + public enum FileFormat { + TEXT(Arrays.asList("doc", "docx", "odt", "rtf", "tex", "txt", "wpd", "md")), + IMAGE(Arrays.asList("tif", "tiff", "bmp", "gif", "jpg", "jpeg", "png", "eps", "raw")), + VIDEO(Arrays.asList("3g2", "3gp", "avi", "flv", "h264", "m4v", "mkv", "mov", "mp4", + "mpg", "mpeg", "rm", "swf", "vob", "wmv")), + AUDIO(Arrays.asList("aif", "cda", "mid", "midi", "mp3", "mpa", "ogg", "wav", "wma", "wpl")), + SHEET(Arrays.asList("xlsx", "xls", "xlsm", "xlt", "xltx", "xltm", "ods", "csv", "tsv", "tab")), + PDF(Arrays.asList("pdf")); + + private final List extensions; + + FileFormat(List extensions) { + this.extensions = extensions; + } + + /** + * Trouve le format correspondant à une extension donnée + */ + /** + * Trouve le format correspondant à une extension donnée + * Retourne TEXT par défaut si l'extension n'est pas reconnue + */ + public static FileFormat fromExtension(String extension) { + if (extension == null) { + return FileFormat.TEXT; + } + + String lowerExt = extension.toLowerCase(); + + return Stream.of(FileFormat.values()) + .filter(format -> format.hasExtension(lowerExt)) + .findFirst() + .orElse(FileFormat.TEXT); + } + + public List getExtensions() { + return extensions; + } + + /** + * Vérifie si cette extension appartient à ce format + */ + public boolean hasExtension(String extension) { + if (extension == null) { + return false; + } + return extensions.contains(extension.toLowerCase()); + } + } + + +} \ No newline at end of file diff --git a/backend/src/main/java/fr/cgi/magneto/core/enums/SlideResourceType.java b/backend/src/main/java/fr/cgi/magneto/core/enums/SlideResourceType.java new file mode 100644 index 000000000..c1044e903 --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/core/enums/SlideResourceType.java @@ -0,0 +1,37 @@ +package fr.cgi.magneto.core.enums; + +public enum SlideResourceType { + TITLE("title"), + DESCRIPTION("description"), + TEXT("text"), + IMAGE("image"), + VIDEO("video"), + AUDIO("audio"), + PDF("pdf"), + SHEET("sheet"), + FILE("file"), + LINK("link"), + BOARD("board"), + EMBEDDER("embedder"), + HYPERLINK("hyperlink"), + DEFAULT("default"); + + private final String value; + + SlideResourceType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static SlideResourceType fromString(String text) { + for (SlideResourceType type : SlideResourceType.values()) { + if (type.value.equalsIgnoreCase(text)) { + return type; + } + } + return DEFAULT; + } +} \ No newline at end of file diff --git a/backend/src/main/java/fr/cgi/magneto/factory/SlideFactory.java b/backend/src/main/java/fr/cgi/magneto/factory/SlideFactory.java new file mode 100644 index 000000000..b35c181c0 --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/factory/SlideFactory.java @@ -0,0 +1,50 @@ +package fr.cgi.magneto.factory; + +import fr.cgi.magneto.core.enums.SlideResourceType; +import fr.cgi.magneto.model.properties.SlideProperties; +import fr.cgi.magneto.model.slides.*; + +public class SlideFactory { + + public Slide createSlide(SlideResourceType type, SlideProperties properties) { + if (!properties.isValidForType(type)) { + throw new IllegalArgumentException("Invalid properties for slide type: " + type); + } + + switch (type) { + case TITLE: + return new SlideTitle(properties.getTitle(), properties.getDescription(), properties.getOwnerName(), + properties.getModificationDate(), properties.getResourceData(), properties.getContentType()); + case DESCRIPTION: + return new SlideDescription(properties.getTitle(), properties.getDescription()); + case TEXT: + return new SlideText(properties.getTitle(), properties.getDescription()); + case FILE: + case PDF: + return new SlideFile(properties.getTitle(), properties.getDescription(), properties.getFileNameString(), properties.getCaption(), properties.getResourceData(), properties.getContentType()); + case LINK: + case HYPERLINK: + case EMBEDDER: + return new SlideLink(properties.getTitle(), properties.getDescription(), properties.getResourceUrl(), properties.getCaption(), properties.getResourceData(), properties.getContentType()); + case IMAGE: + case VIDEO: + case AUDIO: + return new SlideMedia(properties.getTitle(), properties.getCaption(), + properties.getResourceData(), properties.getContentType()); + case BOARD: + return new SlideBoard( + properties.getTitle(), properties.getDescription(), + properties.getOwnerName(), + properties.getModificationDate(), + properties.getResourceNumber(), + properties.getIsShare(), + properties.getIsPublic(), + properties.getCaption(), + properties.getLink(), + properties.getContentType(), properties.getResourceData(), properties.getI18ns()); + + default: + throw new IllegalArgumentException("Unsupported slide type: " + type); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/fr/cgi/magneto/helper/SlideHelper.java b/backend/src/main/java/fr/cgi/magneto/helper/SlideHelper.java new file mode 100644 index 000000000..94622b915 --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/helper/SlideHelper.java @@ -0,0 +1,649 @@ +package fr.cgi.magneto.helper; + +import fr.cgi.magneto.core.constants.CollectionsConstant; +import fr.cgi.magneto.core.constants.Slideshow; +import fr.cgi.magneto.model.slides.SlideMedia; +import io.vertx.core.json.JsonObject; +import org.apache.poi.openxml4j.opc.*; +import org.apache.poi.sl.usermodel.PictureData.PictureType; +import org.apache.poi.sl.usermodel.Placeholder; +import org.apache.poi.sl.usermodel.PlaceholderDetails; +import org.apache.poi.sl.usermodel.TextParagraph.TextAlign; +import org.apache.poi.xslf.usermodel.*; +import org.apache.xmlbeans.XmlCursor; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.openxmlformats.schemas.drawingml.x2006.main.CTHyperlink; +import org.openxmlformats.schemas.presentationml.x2006.main.*; + +import javax.imageio.ImageIO; +import javax.xml.namespace.QName; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Set; + +import static org.apache.poi.openxml4j.opc.PackageRelationshipTypes.CORE_PROPERTIES_ECMA376_NS; + +public class SlideHelper { + + public static XSLFTextBox createTitle(XSLFSlide slide, String title, int titleHeight, Double titleFontSize, + TextAlign titleTextAlign) { + XSLFTextShape titleShape = slide.createTextBox(); + titleShape.setAnchor( + new Rectangle(Slideshow.MARGIN_LEFT, Slideshow.MARGIN_TOP_TITLE, Slideshow.WIDTH, titleHeight)); + + PlaceholderDetails phDetails = titleShape.getPlaceholderDetails(); + if (phDetails != null) { + phDetails.setPlaceholder(Placeholder.TITLE); + } + + titleShape.clearText(); + titleShape.setText(title); + + XSLFTextParagraph para = titleShape.getTextParagraphs().get(0); + para.setTextAlign(titleTextAlign); + + XSLFTextRun run = para.getTextRuns().get(0); + run.setFontSize(titleFontSize); + + return (XSLFTextBox) titleShape; + } + + public static void addNotes(XSLFSlide newSlide, String description) { + Document doc = Jsoup.parse(description); + doc.select("style").remove(); + String text = doc.text(); + + XSLFNotes note = newSlide.getSlideShow().getNotesSlide(newSlide); + + // insert text + for (XSLFTextShape shape : note.getPlaceholders()) { + if (shape.getTextType() == Placeholder.BODY) { + shape.setText(text); + break; + } + } + } + + public static XSLFTextBox createLegend(XSLFSlide slide, String legendText) { + XSLFTextBox legendShape = slide.createTextBox(); + + int slideHeight = slide.getSlideShow().getPageSize().height; + int legendY = slideHeight - Slideshow.LEGEND_HEIGHT - Slideshow.LEGEND_MARGIN_BOTTOM; + + legendShape.setAnchor(new Rectangle(Slideshow.MARGIN_LEFT, legendY, Slideshow.WIDTH, Slideshow.LEGEND_HEIGHT)); + + legendShape.clearText(); + legendShape.setText(legendText); + + XSLFTextParagraph para = legendShape.getTextParagraphs().get(0); + para.setTextAlign(TextAlign.LEFT); + + XSLFTextRun run = para.getTextRuns().get(0); + run.setFontSize(Slideshow.LEGEND_FONT_SIZE); + run.setFontFamily(Slideshow.LEGEND_FONT_FAMILY); + + return legendShape; + } + + public static XSLFTextBox createContent(XSLFSlide slide) { + XSLFTextBox contentBox = slide.createTextBox(); + contentBox.setAnchor(new Rectangle(Slideshow.MARGIN_LEFT, Slideshow.CONTENT_MARGIN_TOP, Slideshow.WIDTH, + Slideshow.CONTENT_HEIGHT)); + return contentBox; + } + + public static XSLFPictureShape createImage(XSLFSlide slide, byte[] pictureData, String fileContentType, + int contentMarginTop, int imageContentHeight, Boolean alignLeft) { + XMLSlideShow ppt = slide.getSlideShow(); + + XSLFPictureData pic = ppt.addPicture(pictureData, getPictureTypeFromContentType(fileContentType)); + + java.awt.Dimension imgSize = pic.getImageDimension(); + + int availableWidth = Slideshow.WIDTH; + + // Calculer les dimensions tout en préservant le ratio + double scaleFactor = Math.min( + (double) availableWidth / imgSize.width, + (double) imageContentHeight / imgSize.height + ); + + int newWidth = (int) (imgSize.width * scaleFactor); + int newHeight = (int) (imgSize.height * scaleFactor); + + // Calculer la position X en fonction de l'alignement + int x = alignLeft ? + Slideshow.MARGIN_LEFT : + Slideshow.MARGIN_LEFT + (availableWidth - newWidth) / 2; + + // Centrer verticalement dans l'espace alloué + int y = contentMarginTop + (imageContentHeight - newHeight) / 2; + + XSLFPictureShape shape = slide.createPicture(pic); + shape.setAnchor(new Rectangle(x, y, newWidth, newHeight)); + + return shape; + } + + public static XSLFPictureShape createImageWidthHeight(XSLFSlide slide, byte[] pictureData, String fileContentType, + int contentMarginTop, int imageContentHeight, int imageContentWidth, Boolean alignLeft) { + XMLSlideShow ppt = slide.getSlideShow(); + + XSLFPictureData pic = ppt.addPicture(pictureData, getPictureTypeFromContentType(fileContentType)); + + int x = alignLeft ? Slideshow.MARGIN_LEFT : Slideshow.MARGIN_LEFT + (Slideshow.WIDTH - imageContentWidth) / 2; + + XSLFPictureShape shape = slide.createPicture(pic); + shape.setAnchor(new Rectangle(x, contentMarginTop, imageContentWidth, imageContentHeight)); + + return shape; + } + + public static XSLFTextBox createLink(XSLFSlide slide, String url) { + // Créer une zone de texte pour le lien + XSLFTextBox linkBox = slide.createTextBox(); + int linkPositionY = Slideshow.MARGIN_TOP_TITLE + Slideshow.TITLE_HEIGHT + 10; + // Positionner la zone de texte en utilisant les constantes existantes + + linkBox.setAnchor(new Rectangle( + Slideshow.MARGIN_LEFT, + linkPositionY, + Slideshow.WIDTH, + 50)); + + // Créer un paragraphe pour le texte du lien + XSLFTextParagraph paragraph = linkBox.addNewTextParagraph(); + paragraph.setTextAlign(TextAlign.LEFT); + + // Créer un TextRun avec l'URL comme texte affiché + XSLFTextRun textRun = paragraph.addNewTextRun(); + textRun.setText(url); + textRun.setFontSize(Slideshow.CONTENT_FONT_SIZE); + textRun.setFontColor(new Color(0, 0, 255)); // Bleu pour indiquer un lien + textRun.setUnderlined(true); // Souligné pour indiquer un lien + + // Utiliser XSLFHyperlink pour créer le lien + XSLFHyperlink hyperlink = textRun.createHyperlink(); + hyperlink.setAddress(url); + + return linkBox; + } + + public static XSLFTextBox createBoardInfoList(XSLFSlide slide, String ownerName, String modificationDate, + int resourceNumber, + boolean isShare, boolean isPublic, JsonObject i18ns) { + // Créer une zone de texte pour la liste + XSLFTextBox infoBox = slide.createTextBox(); + + int listPositionY = Slideshow.MAIN_CONTENT_MARGIN_TOP; + infoBox.setAnchor( + new Rectangle(Slideshow.WIDTH - Slideshow.MARGIN_LEFT * 5, listPositionY, Slideshow.BOARD_TEXT_WIDTH, 200)); + + XSLFTextParagraph paragraph1 = infoBox.addNewTextParagraph(); + paragraph1.setSpaceBefore(0.0); + XSLFTextRun textRun1 = paragraph1.addNewTextRun(); + textRun1.setText("• " + i18ns.getString(CollectionsConstant.I18N_SLIDESHOW_OWNER) + ownerName); + textRun1.setFontSize(Slideshow.CONTENT_FONT_SIZE); + + if (modificationDate != null && !modificationDate.isEmpty()) { + XSLFTextParagraph paragraphDate = infoBox.addNewTextParagraph(); + XSLFTextRun textRunDate = paragraphDate.addNewTextRun(); + textRunDate.setText( + "• " + i18ns.getString(CollectionsConstant.I18N_SLIDESHOW_UPDATED) + formatDate(modificationDate)); + textRunDate.setFontSize(Slideshow.CONTENT_FONT_SIZE); + } + + XSLFTextParagraph paragraph2 = infoBox.addNewTextParagraph(); + XSLFTextRun textRun2 = paragraph2.addNewTextRun(); + textRun2.setText("• " + resourceNumber + " " + i18ns.getString(CollectionsConstant.I18N_SLIDESHOW_MAGNETS)); + textRun2.setFontSize(Slideshow.CONTENT_FONT_SIZE); + + // 3. Tableau partagé (si applicable) + if (isShare) { + XSLFTextParagraph paragraph3 = infoBox.addNewTextParagraph(); + XSLFTextRun textRun3 = paragraph3.addNewTextRun(); + textRun3.setText("• " + i18ns.getString(CollectionsConstant.I18N_SLIDESHOW_SHARED)); + textRun3.setFontSize(Slideshow.CONTENT_FONT_SIZE); + } + + // 4. Tableau de la plateforme + if (isPublic) { + XSLFTextParagraph paragraph4 = infoBox.addNewTextParagraph(); + XSLFTextRun textRun4 = paragraph4.addNewTextRun(); + textRun4.setText("• " + i18ns.getString(CollectionsConstant.I18N_SLIDESHOW_PLATFORM)); + textRun4.setFontSize(Slideshow.CONTENT_FONT_SIZE); + } + + return infoBox; + } + + public static XSLFPictureShape createMedia(XSLFSlide slide, byte[] mediaData, String fileContentType, + SlideMedia.MediaType mediaType) { + + if (mediaType != SlideMedia.MediaType.AUDIO && mediaType != SlideMedia.MediaType.VIDEO) { + throw new IllegalArgumentException("Type de média non supporté: " + mediaType); + } + + boolean isAudio = mediaType == SlideMedia.MediaType.AUDIO; + String extension = getExtensionFromContentType(fileContentType); + + try { + // Générer un nom pour le fichier média + String mediaTypeStr = isAudio ? Slideshow.MEDIA_TYPE_AUDIO : Slideshow.MEDIA_TYPE_VIDEO; + String mediaFileName = mediaTypeStr + "_" + System.currentTimeMillis() + "." + extension; + + // Créer et stocker le fichier média + XMLSlideShow ppt = slide.getSlideShow(); + OPCPackage opcPackage = ppt.getPackage(); + + PackagePartName mediaPartName = PackagingURIHelper + .createPartName(Slideshow.MEDIA_PATH_PREFIX + mediaFileName); + PackagePart mediaPart = opcPackage.createPart(mediaPartName, fileContentType); + + try (OutputStream out = mediaPart.getOutputStream()) { + out.write(mediaData); + } + + // Obtenir la partie du slide + PackagePart pp = slide.getPackagePart(); + + // Créer deux relations vers le fichier média + PackageRelationship prsEmbed = pp.addRelationship( + mediaPart.getPartName(), TargetMode.INTERNAL, + Slideshow.RELATIONSHIP_MEDIA); + + String execRelationship = isAudio ? Slideshow.RELATIONSHIP_AUDIO : Slideshow.RELATIONSHIP_VIDEO; + PackageRelationship prsExec = pp.addRelationship( + mediaPart.getPartName(), TargetMode.INTERNAL, + execRelationship); + + // Créer l'icône ou la miniature + byte[] iconData = isAudio ? getAudioIcon() : getVideoThumbnail(mediaData, extension); + XSLFPictureData snap = ppt.addPicture(iconData, PictureType.PNG); + + XSLFPictureShape pic = isAudio ? createAndPositionMediaIcon(slide, iconData) + : createAndPositionVideoThumbnail(slide, iconData); + + // Configurer les propriétés de l'image pour le média + CTPicture xpic = (CTPicture) pic.getXmlObject(); + + CTHyperlink link = xpic.getNvPicPr().getCNvPr().addNewHlinkClick(); + link.setId(""); + link.setAction(Slideshow.ACTION_MEDIA); + + // Ajouter les propriétés au média + CTApplicationNonVisualDrawingProps nvPr = xpic.getNvPicPr().getNvPr(); + if (isAudio) { + nvPr.addNewAudioFile().setLink(prsExec.getId()); + } else { + nvPr.addNewVideoFile().setLink(prsExec.getId()); + } + + // Ajouter l'extension média + CTExtension ext = nvPr.addNewExtLst().addNewExt(); + ext.setUri(Slideshow.EXTENSION_URI_MEDIA); + + // Configurer l'élément p14:media + try (XmlCursor cur = ext.newCursor()) { + cur.toEndToken(); + cur.beginElement(new QName(Slideshow.NAMESPACE_POWERPOINT_2010, "media", "p14")); + cur.insertNamespace("p14", Slideshow.NAMESPACE_POWERPOINT_2010); + cur.insertNamespace("r", CORE_PROPERTIES_ECMA376_NS); + cur.insertAttributeWithValue( + new QName(CORE_PROPERTIES_ECMA376_NS, "embed"), + prsEmbed.getId()); + } + + // S'assurer que le blipFill utilise le bon ID pour l'image + String imageRelId = slide.getRelationId(snap); + if (imageRelId != null) { + xpic.getBlipFill().getBlip().setEmbed(imageRelId); + } + + // Ajouter la section timing - CRUCIAL + CTSlide xslide = slide.getXmlObject(); + CTTimeNodeList ctnl; + + if (!xslide.isSetTiming()) { + CTTLCommonTimeNodeData ctn = xslide.addNewTiming().addNewTnLst().addNewPar().addNewCTn(); + ctn.setId(Slideshow.TIMING_ROOT_ID); + ctn.setDur(STTLTimeIndefinite.INDEFINITE); + ctn.setRestart(STTLTimeNodeRestartType.NEVER); + ctn.setNodeType(STTLTimeNodeType.TM_ROOT); + ctnl = ctn.addNewChildTnLst(); + } else { + ctnl = xslide.getTiming().getTnLst().getParArray(0).getCTn().getChildTnLst(); + } + + // Ajouter le nœud média approprié (audio ou vidéo) + CTTLCommonMediaNodeData cmedia = isAudio ? ctnl.addNewAudio().addNewCMediaNode() + : ctnl.addNewVideo().addNewCMediaNode(); + cmedia.setVol(Slideshow.MEDIA_VOLUME); + + CTTLCommonTimeNodeData ctn = cmedia.addNewCTn(); + ctn.setId(Slideshow.TIMING_MEDIA_ID); + ctn.setFill(STTLTimeNodeFillType.HOLD); + ctn.setDisplay(false); + + ctn.addNewStCondLst().addNewCond().setDelay(STTLTimeIndefinite.INDEFINITE); + + cmedia.addNewTgtEl().addNewSpTgt().setSpid(pic.getShapeId()); + + return pic; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + private static PictureType getPictureTypeFromContentType(String contentType) { + if (contentType == null) { + return PictureType.PNG; + } + + String lowerContentType = contentType.toLowerCase(); + + switch (lowerContentType) { + case Slideshow.CONTENT_TYPE_IMAGE_JPEG: + case Slideshow.CONTENT_TYPE_IMAGE_JPG: + return PictureType.JPEG; + case Slideshow.CONTENT_TYPE_IMAGE_PNG: + return PictureType.PNG; + case Slideshow.CONTENT_TYPE_IMAGE_GIF: + return PictureType.GIF; + case Slideshow.CONTENT_TYPE_IMAGE_TIFF: + return PictureType.TIFF; + case Slideshow.CONTENT_TYPE_IMAGE_X_EMF: + return PictureType.EMF; + case Slideshow.CONTENT_TYPE_IMAGE_X_WMF: + return PictureType.WMF; + case Slideshow.CONTENT_TYPE_IMAGE_X_PICT: + return PictureType.PICT; + case Slideshow.CONTENT_TYPE_IMAGE_DIB: + return PictureType.DIB; + case Slideshow.CONTENT_TYPE_IMAGE_X_EPS: + return PictureType.EPS; + case Slideshow.CONTENT_TYPE_IMAGE_X_MS_BMP: + case Slideshow.CONTENT_TYPE_IMAGE_BMP: + return PictureType.BMP; + case Slideshow.CONTENT_TYPE_IMAGE_X_WPG: + return PictureType.WPG; + case Slideshow.CONTENT_TYPE_IMAGE_VND_MS_PHOTO: + return PictureType.WDP; + case Slideshow.CONTENT_TYPE_IMAGE_SVG_XML: + return PictureType.SVG; + default: + return PictureType.PNG; + } + } + + private static XSLFPictureShape createAndPositionMediaIcon(XSLFSlide slide, byte[] iconData) { + + XMLSlideShow ppt = slide.getSlideShow(); + XSLFPictureData snap = ppt.addPicture(iconData, PictureType.PNG); + + XSLFPictureShape pic = slide.createPicture(snap); + + // Définir une taille plus grande + int iconWidth = Slideshow.ICON_WIDTH; + int iconHeight = Slideshow.ICON_HEIGHT; + + // Calculer la position verticale centrée + int y = (Slideshow.SLIDE_HEIGHT - iconHeight) / 2; // Centre vertical + + // Utiliser le MARGIN_LEFT existant pour l'alignement horizontal + pic.setAnchor(new Rectangle(Slideshow.MARGIN_LEFT, y, iconWidth, iconHeight)); + + return pic; + } + + private static byte[] getAudioIcon() { + try { + BufferedImage image = new BufferedImage( + Slideshow.AUDIO_ICON_WIDTH, + Slideshow.AUDIO_ICON_HEIGHT, + BufferedImage.TYPE_INT_ARGB); + + Graphics2D g2d = image.createGraphics(); + + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + g2d.setColor(Slideshow.AUDIO_ICON_BACKGROUND_COLOR); + g2d.fillOval( + Slideshow.AUDIO_ICON_CIRCLE_X, + Slideshow.AUDIO_ICON_CIRCLE_Y, + Slideshow.AUDIO_ICON_CIRCLE_WIDTH, + Slideshow.AUDIO_ICON_CIRCLE_HEIGHT); + + g2d.setColor(Slideshow.AUDIO_ICON_PLAY_COLOR); + g2d.fillPolygon( + Slideshow.AUDIO_ICON_TRIANGLE_X, + Slideshow.AUDIO_ICON_TRIANGLE_Y, + Slideshow.AUDIO_ICON_TRIANGLE_X.length); + + // Libérer les ressources + g2d.dispose(); + + // Convertir l'image en tableau de bytes (PNG) + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(image, Slideshow.AUDIO_ICON_FORMAT, baos); + return baos.toByteArray(); + + } catch (IOException e) { + e.printStackTrace(); + return Slideshow.FALLBACK_PNG; + } + } + + private static XSLFPictureShape createAndPositionVideoThumbnail(XSLFSlide slide, byte[] thumbnailData) { + + XMLSlideShow ppt = slide.getSlideShow(); + XSLFPictureData snap = ppt.addPicture(thumbnailData, PictureType.PNG); + + XSLFPictureShape pic = slide.createPicture(snap); + + int videoWidth = Slideshow.VIDEO_DISPLAY_WIDTH; + int videoHeight = Slideshow.VIDEO_DISPLAY_HEIGHT; + + int x = (Slideshow.SLIDE_WIDTH - videoWidth) / 2; // Centre horizontal + int y = (Slideshow.SLIDE_HEIGHT - videoHeight) / 2; // Centre vertical + + pic.setAnchor(new Rectangle(x, y, videoWidth, videoHeight)); + + return pic; + } + + private static byte[] getVideoThumbnail(byte[] videoData, String extension) { + try { + // Dans un cas réel, on extrairait une image de la vidéo ici + // Pour cet exemple, nous allons simplement créer une vignette générique + + // Créer une image au format 16:9 + BufferedImage image = new BufferedImage( + Slideshow.VIDEO_THUMBNAIL_WIDTH, + Slideshow.VIDEO_THUMBNAIL_HEIGHT, + BufferedImage.TYPE_INT_RGB); + + // Obtenir le contexte graphique + Graphics2D g2d = image.createGraphics(); + + // Activer l'antialiasing pour des bords plus lisses + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // Remplir le fond avec un dégradé bleu foncé + g2d.setColor(Slideshow.VIDEO_BACKGROUND_COLOR); + g2d.fillRect(0, 0, Slideshow.VIDEO_THUMBNAIL_WIDTH, Slideshow.VIDEO_THUMBNAIL_HEIGHT); + + // Dessiner un symbole de lecture au centre + g2d.setColor(Slideshow.VIDEO_PLAY_BUTTON_COLOR); + int centerX = Slideshow.VIDEO_THUMBNAIL_WIDTH / 2; + int centerY = Slideshow.VIDEO_THUMBNAIL_HEIGHT / 2; + int radius = Slideshow.VIDEO_PLAY_BUTTON_RADIUS; + g2d.fillOval(centerX - radius, centerY - radius, radius * 2, radius * 2); + + // Triangle de lecture + g2d.setColor(Slideshow.VIDEO_BACKGROUND_COLOR); + int[] xPoints = { + centerX - Slideshow.VIDEO_PLAY_TRIANGLE_OFFSET_X, + centerX + Slideshow.VIDEO_PLAY_TRIANGLE_OFFSET_X2, + centerX - Slideshow.VIDEO_PLAY_TRIANGLE_OFFSET_X + }; + int[] yPoints = { + centerY - Slideshow.VIDEO_PLAY_TRIANGLE_OFFSET_Y, + centerY, + centerY + Slideshow.VIDEO_PLAY_TRIANGLE_OFFSET_Y + }; + g2d.fillPolygon(xPoints, yPoints, 3); + + // Ajouter le texte "VIDÉO" + g2d.setColor(Slideshow.VIDEO_TEXT_COLOR); + g2d.setFont(new java.awt.Font( + Slideshow.VIDEO_THUMBNAIL_FONT, + Slideshow.VIDEO_THUMBNAIL_FONT_STYLE, + Slideshow.VIDEO_THUMBNAIL_FONT_SIZE)); + java.awt.FontMetrics fm = g2d.getFontMetrics(); + String text = Slideshow.VIDEO_THUMBNAIL_TEXT; + int textWidth = fm.stringWidth(text); + g2d.drawString(text, centerX - textWidth / 2, centerY + Slideshow.VIDEO_TEXT_Y_OFFSET); + + // Libérer les ressources + g2d.dispose(); + + // Convertir l'image en tableau de bytes (PNG) + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(image, Slideshow.VIDEO_THUMBNAIL_FORMAT, baos); + return baos.toByteArray(); + + } catch (IOException e) { + e.printStackTrace(); + return getDefaultThumbnail(); + } + } + + private static byte[] getDefaultThumbnail() { + try { + // Créer une image simple par défaut + BufferedImage image = new BufferedImage(Slideshow.THUMBNAIL_WIDTH, Slideshow.THUMBNAIL_HEIGHT, + BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = image.createGraphics(); + g2d.setColor(Color.DARK_GRAY); + g2d.fillRect(0, 0, Slideshow.THUMBNAIL_WIDTH, Slideshow.THUMBNAIL_HEIGHT); + g2d.setColor(Color.WHITE); + g2d.setFont(new java.awt.Font(Slideshow.THUMBNAIL_FONT, Slideshow.THUMBNAIL_FONT_STYLE, + Slideshow.THUMBNAIL_FONT_SIZE)); + g2d.drawString(Slideshow.THUMBNAIL_DEFAULT_TEXT, Slideshow.THUMBNAIL_TEXT_X, Slideshow.THUMBNAIL_TEXT_Y); + g2d.dispose(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(image, Slideshow.THUMBNAIL_FORMAT, baos); + return baos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + return new byte[0]; + } + } + + private static String getExtensionFromContentType(String contentType) { + if (contentType == null) { + return Slideshow.DEFAULT_VIDEO_EXTENSION; + } + + String lowerContentType = contentType.toLowerCase(); + + switch (lowerContentType) { + // Types audio + case Slideshow.CONTENT_TYPE_AUDIO_MPEG: + case Slideshow.CONTENT_TYPE_AUDIO_MP3: + return Slideshow.EXT_MP3; + case Slideshow.CONTENT_TYPE_AUDIO_WAV: + case Slideshow.CONTENT_TYPE_AUDIO_X_WAV: + return Slideshow.EXT_WAV; + case Slideshow.CONTENT_TYPE_AUDIO_MP4: + case Slideshow.CONTENT_TYPE_AUDIO_X_M4A: + return Slideshow.EXT_M4A; + case Slideshow.CONTENT_TYPE_AUDIO_OGG: + return Slideshow.EXT_OGG; + + // Types vidéo + case Slideshow.CONTENT_TYPE_VIDEO_MP4: + return Slideshow.EXT_MP4; + case Slideshow.CONTENT_TYPE_VIDEO_MPEG: + return Slideshow.EXT_MPG; + case Slideshow.CONTENT_TYPE_VIDEO_X_MS_WMV: + return Slideshow.EXT_WMV; + case Slideshow.CONTENT_TYPE_VIDEO_QUICKTIME: + return Slideshow.EXT_MOV; + case Slideshow.CONTENT_TYPE_VIDEO_X_MATROSKA: + return Slideshow.EXT_MKV; + case Slideshow.CONTENT_TYPE_VIDEO_WEBM: + return Slideshow.EXT_WEBM; + case Slideshow.CONTENT_TYPE_VIDEO_X_FLV: + return Slideshow.EXT_FLV; + case Slideshow.CONTENT_TYPE_VIDEO_3GPP: + return Slideshow.EXT_3GP; + case Slideshow.CONTENT_TYPE_VIDEO_AVI: + case Slideshow.CONTENT_TYPE_VIDEO_X_MSVIDEO: + return Slideshow.EXT_AVI; + + default: + if (lowerContentType.startsWith(Slideshow.CONTENT_TYPE_AUDIO)) { + return Slideshow.DEFAULT_AUDIO_EXTENSION; + } else if (lowerContentType.startsWith(Slideshow.CONTENT_TYPE_VIDEO)) { + return Slideshow.DEFAULT_VIDEO_EXTENSION; + } else { + return null; + } + } + } + + private static String formatDate(String dateString) { + if (dateString == null || dateString.isEmpty()) { + return ""; + } + + try { + java.text.SimpleDateFormat inputFormat = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + java.text.SimpleDateFormat outputFormat = new java.text.SimpleDateFormat("dd/MM/yyyy"); + + java.util.Date date = inputFormat.parse(dateString); + return outputFormat.format(date); + } catch (java.text.ParseException e) { + return dateString; + } + } + + public static String generateUniqueFileName(Set usedFileNames, String originalFileName) { + if (!usedFileNames.contains(originalFileName)) { + usedFileNames.add(originalFileName); + return originalFileName; + } + + String fileNameWithoutExtension = getFileNameWithoutExtension(originalFileName); + String extension = getFileExtension(originalFileName); + + int counter = 1; + String uniqueFileName; + do { + uniqueFileName = fileNameWithoutExtension + " (" + counter + ")." + extension; + counter++; + } while (usedFileNames.contains(uniqueFileName)); + + usedFileNames.add(uniqueFileName); + return uniqueFileName; + } + + private static String getFileNameWithoutExtension(String fileName) { + int dotIndex = fileName.lastIndexOf('.'); + return (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex); + } + + private static String getFileExtension(String fileName) { + int dotIndex = fileName.lastIndexOf('.'); + return (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1); + } +} \ No newline at end of file diff --git a/backend/src/main/java/fr/cgi/magneto/model/Section.java b/backend/src/main/java/fr/cgi/magneto/model/Section.java index 48af01f62..351afd92b 100644 --- a/backend/src/main/java/fr/cgi/magneto/model/Section.java +++ b/backend/src/main/java/fr/cgi/magneto/model/Section.java @@ -1,6 +1,7 @@ package fr.cgi.magneto.model; import fr.cgi.magneto.core.constants.Field; +import fr.cgi.magneto.model.cards.Card; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -13,6 +14,7 @@ public class Section implements Model { private List cardIds; private String boardId; private Boolean displayed; + private List cards; @SuppressWarnings("unchecked") public Section(JsonObject section) { @@ -22,6 +24,8 @@ public Section(JsonObject section) { this.boardId = section.getString(Field.BOARDID); if (section.containsKey(Field.DISPLAYED)) this.displayed = section.getBoolean(Field.DISPLAYED); + if (section.containsKey(Field.CARDS)) + this.cards = section.getJsonArray(Field.CARDS).getList(); } public Section() { @@ -87,6 +91,15 @@ public void setDisplayed(boolean displayed) { this.displayed = displayed; } + public List getCards() { + return cards; + } + + public Section setCards(List cards) { + this.cards = cards; + return this; + } + @Override public JsonObject toJson() { JsonObject json = new JsonObject() @@ -96,6 +109,8 @@ public JsonObject toJson() { .put(Field.BOARDID, this.getBoardId()); if (this.displayed != null) json.put(Field.DISPLAYED, this.getDisplayed()); + if (this.cards != null) + json.put(Field.CARDS, this.getCards()); return json; } diff --git a/backend/src/main/java/fr/cgi/magneto/model/properties/SlideProperties.java b/backend/src/main/java/fr/cgi/magneto/model/properties/SlideProperties.java new file mode 100644 index 000000000..61db4b618 --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/model/properties/SlideProperties.java @@ -0,0 +1,261 @@ +package fr.cgi.magneto.model.properties; + +import fr.cgi.magneto.core.enums.SlideResourceType; +import fr.cgi.magneto.helper.I18nHelper; +import io.vertx.core.json.JsonObject; + +public class SlideProperties { + private String title; + private String description; + private String caption; + private String content; + private String resourceUrl; + private String resourceId; + private String fileNameString; + private byte[] resourceData; + private String contentType; + private String link; + private JsonObject i18ns; + private I18nHelper i18nHelper; + + private String ownerName; + private String modificationDate; + private Integer resourceNumber; + private Boolean isShare; + private Boolean isPublic; + + private SlideProperties() { + } + + private boolean isValidForTitle() { + return title != null && description != null && ownerName != null && modificationDate != null + && resourceData != null && contentType != null; + } + + private boolean isValidForFile() { + return fileNameString != null && resourceData != null && title != null && caption != null && contentType != null; + } + + public boolean isValidForType(SlideResourceType type) { + if (type == null) + return false; + + switch (type) { + case TITLE: + return isValidForTitle(); + case DESCRIPTION: + return isValidForDescription(); + case TEXT: + return isValidForText(); + case FILE: + case PDF: + return isValidForFile(); + case LINK: + case HYPERLINK: + case EMBEDDER: + return isValidForLink(); + case IMAGE: + case VIDEO: + case AUDIO: + return isValidForMedia(); + case BOARD: + return isValidForBoard(); + default: + return false; + } + } + + public I18nHelper getI18nHelper() { + return i18nHelper; + } + + private boolean isValidForDescription() { return title != null; } + + private boolean isValidForText() { + return title != null; + } + + public String getFileNameString() { + return fileNameString; + } + + private boolean isValidForLink() { + return resourceUrl != null && title != null && caption != null && resourceData != null && contentType != null; + } + + private boolean isValidForMedia() { + return title != null && caption != null && contentType != null && resourceData != null; + } + + private boolean isValidForBoard() { + return title != null && ownerName != null + && link != null + && caption != null + && modificationDate != null + && resourceNumber != null + && isShare != null + && contentType != null + && resourceData != null + && isPublic != null + && !i18ns.isEmpty(); + } + + // Getters + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public String getCaption() { + return caption; + } + + public String getContent() { + return content; + } + + public String getResourceUrl() { + return resourceUrl; + } + + public String getResourceId() { + return resourceId; + } + + public String getContentType() { + return contentType; + } + + public String getLink() { + return link; + } + + public JsonObject getI18ns() { + return i18ns; + } + + public static class Builder { + private final SlideProperties properties; + + public Builder() { + properties = new SlideProperties(); + } + + public Builder title(String title) { + properties.title = title; + return this; + } + + public Builder description(String description) { + properties.description = description; + return this; + } + + public Builder caption(String caption) { + properties.caption = caption; + return this; + } + + public Builder content(String content) { + properties.content = content; + return this; + } + + public Builder resourceUrl(String resourceUrl) { + properties.resourceUrl = resourceUrl; + return this; + } + + public Builder resourceId(String resourceId) { + properties.resourceId = resourceId; + return this; + } + + public Builder contentType(String contentType) { + properties.contentType = contentType; + return this; + } + + public Builder fileNameString(String fileNameString) { + properties.fileNameString = fileNameString; + return this; + } + + public Builder link(String link) { + properties.link = link; + return this; + } + + public Builder i18ns(JsonObject i18ns) { + properties.i18ns = i18ns; + return this; + } + + public Builder resourceData(byte[] resourceData) { + properties.resourceData = resourceData; + return this; + } + + // Propriétés spécifiques board + public Builder ownerName(String ownerName) { + properties.ownerName = ownerName; + return this; + } + + public Builder modificationDate(String modificationDate) { + properties.modificationDate = modificationDate; + return this; + } + + public Builder resourceNumber(Integer resourceNumber) { + properties.resourceNumber = resourceNumber; + return this; + } + + public Builder isShare(Boolean isShare) { + properties.isShare = isShare; + return this; + } + + public Builder isPublic(Boolean isPublic) { + properties.isPublic = isPublic; + return this; + } + + public Builder i18nHelper(I18nHelper i18nHelper) { + properties.i18nHelper = i18nHelper; + return this; + } + + public SlideProperties build() { + return properties; + } + } + + public byte[] getResourceData() { + return resourceData; + } + + public String getOwnerName() { + return ownerName; + } + + public String getModificationDate() { + return modificationDate; + } + + public Integer getResourceNumber() { + return resourceNumber; + } + + public Boolean getIsShare() { + return isShare; + } + + public Boolean getIsPublic() { + return isPublic; + } +} \ No newline at end of file diff --git a/backend/src/main/java/fr/cgi/magneto/model/slides/Slide.java b/backend/src/main/java/fr/cgi/magneto/model/slides/Slide.java new file mode 100644 index 000000000..fb5b10f15 --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/model/slides/Slide.java @@ -0,0 +1,10 @@ +package fr.cgi.magneto.model.slides; + +import org.apache.poi.xslf.usermodel.XSLFSlide; + +public abstract class Slide { + protected String title; + protected String description = ""; + + public abstract Object createApacheSlide(XSLFSlide newSlide); +} diff --git a/backend/src/main/java/fr/cgi/magneto/model/slides/SlideBoard.java b/backend/src/main/java/fr/cgi/magneto/model/slides/SlideBoard.java new file mode 100644 index 000000000..f46187c18 --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/model/slides/SlideBoard.java @@ -0,0 +1,54 @@ +package fr.cgi.magneto.model.slides; + +import fr.cgi.magneto.core.constants.Slideshow; +import fr.cgi.magneto.helper.SlideHelper; +import io.vertx.core.json.JsonObject; +import org.apache.poi.sl.usermodel.TextParagraph; +import org.apache.poi.xslf.usermodel.XSLFSlide; + +public class SlideBoard extends Slide { + private final String ownerName; + private final String link; + private final String modificationDate; + private final int resourceNumber; + private final boolean isShare; + private final boolean isPublic; + private final String caption; + private final String fileContentType; + private byte[] resourceData; + private JsonObject i18ns; + + public SlideBoard(String title, String description, String ownerName, String modificationDate, int resourceNumber, + boolean isShare, boolean isPublic, String caption, String link, String contentType, + byte[] resourceData, JsonObject i18ns) { + this.title = title; + this.description = description; + this.ownerName = ownerName; + this.modificationDate = modificationDate; + this.resourceNumber = resourceNumber; + this.caption = caption; + this.isShare = isShare; + this.isPublic = isPublic; + this.link = link; + this.fileContentType = contentType; + this.resourceData = resourceData; + this.i18ns = i18ns; + + } + + @Override + public Object createApacheSlide(XSLFSlide newSlide) { + SlideHelper.createTitle(newSlide, title, Slideshow.TITLE_HEIGHT, Slideshow.TITLE_FONT_SIZE, + TextParagraph.TextAlign.LEFT); + SlideHelper.createLink(newSlide, link); + SlideHelper.createImage(newSlide, resourceData, fileContentType, Slideshow.MAIN_CONTENT_MARGIN_TOP, + Slideshow.BOARD_IMAGE_CONTENT_HEIGHT, true); + SlideHelper.createBoardInfoList(newSlide, ownerName, modificationDate, resourceNumber, isShare, isPublic, + i18ns); + SlideHelper.createLegend(newSlide, caption); + + SlideHelper.addNotes(newSlide, description); + + return newSlide; + } +} diff --git a/backend/src/main/java/fr/cgi/magneto/model/slides/SlideDescription.java b/backend/src/main/java/fr/cgi/magneto/model/slides/SlideDescription.java new file mode 100644 index 000000000..d9a0e7fab --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/model/slides/SlideDescription.java @@ -0,0 +1,35 @@ +package fr.cgi.magneto.model.slides; + +import fr.cgi.magneto.core.constants.Slideshow; +import fr.cgi.magneto.helper.SlideHelper; +import org.apache.poi.sl.usermodel.TextParagraph; +import org.apache.poi.xslf.usermodel.XSLFSlide; +import org.apache.poi.xslf.usermodel.XSLFTextBox; +import org.apache.poi.xslf.usermodel.XSLFTextParagraph; +import org.apache.poi.xslf.usermodel.XSLFTextRun; + +public class SlideDescription extends Slide { + + public SlideDescription(String title, String description) { + this.title = title; + this.description = description; + } + + @Override + public Object createApacheSlide(XSLFSlide newSlide) { + + SlideHelper.createTitle(newSlide, title, + Slideshow.DESCRIPTION_TITLE_HEIGHT, Slideshow.DESCRIPTION_TITLE_FONT_SIZE, TextParagraph.TextAlign.LEFT); + + XSLFTextBox textBox = SlideHelper.createContent(newSlide); + + XSLFTextParagraph paragraph = textBox.addNewTextParagraph(); + paragraph.setTextAlign(TextParagraph.TextAlign.LEFT); + XSLFTextRun textRun = paragraph.addNewTextRun(); + textRun.setText(description); + textRun.setFontSize(Slideshow.DESCRIPTION_FONT_SIZE); + textRun.setFontFamily(Slideshow.DEFAULT_FONT); + + return newSlide; + } +} diff --git a/backend/src/main/java/fr/cgi/magneto/model/slides/SlideFile.java b/backend/src/main/java/fr/cgi/magneto/model/slides/SlideFile.java new file mode 100644 index 000000000..6dcf2afa7 --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/model/slides/SlideFile.java @@ -0,0 +1,48 @@ +package fr.cgi.magneto.model.slides; + +import fr.cgi.magneto.core.constants.Slideshow; +import fr.cgi.magneto.helper.SlideHelper; +import org.apache.poi.sl.usermodel.TextParagraph; +import org.apache.poi.xslf.usermodel.XSLFSlide; +import org.apache.poi.xslf.usermodel.XSLFTextBox; +import org.apache.poi.xslf.usermodel.XSLFTextParagraph; +import org.apache.poi.xslf.usermodel.XSLFTextRun; + +public class SlideFile extends Slide { + private final String filenameString; + private final String caption; + private final byte[] fileSvg; + private final String fileContentType; + + public SlideFile(String title, String description, String filenameString, String caption, byte[] fileSvg, String fileContentType) { + this.title = title; + this.description = description; + this.filenameString = filenameString; + this.caption = caption; + this.fileSvg = fileSvg; + this.fileContentType = fileContentType; + } + + @Override + public Object createApacheSlide(XSLFSlide newSlide) { + SlideHelper.createTitle(newSlide, title, Slideshow.TITLE_HEIGHT, Slideshow.DESCRIPTION_TITLE_FONT_SIZE, + TextParagraph.TextAlign.LEFT); + + XSLFTextBox textBox = SlideHelper.createContent(newSlide); + XSLFTextParagraph paragraph = textBox.addNewTextParagraph(); + paragraph.setTextAlign(TextParagraph.TextAlign.LEFT); + paragraph.setLineSpacing(175.0); + XSLFTextRun textRun = paragraph.addNewTextRun(); + textRun.setText(filenameString); + textRun.setFontSize(Slideshow.CONTENT_FONT_SIZE); + textRun.setFontFamily(Slideshow.DEFAULT_FONT); + + SlideHelper.createImageWidthHeight(newSlide, fileSvg, fileContentType, Slideshow.MAIN_CONTENT_MARGIN_TOP, + Slideshow.SVG_CONTENT_HEIGHT, Slideshow.SVG_CONTENT_WIDTH, true); + SlideHelper.createLegend(newSlide, caption); + + SlideHelper.addNotes(newSlide, description); + + return newSlide; + } +} diff --git a/backend/src/main/java/fr/cgi/magneto/model/slides/SlideLink.java b/backend/src/main/java/fr/cgi/magneto/model/slides/SlideLink.java new file mode 100644 index 000000000..fa388eade --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/model/slides/SlideLink.java @@ -0,0 +1,37 @@ +package fr.cgi.magneto.model.slides; + +import fr.cgi.magneto.core.constants.Slideshow; +import fr.cgi.magneto.helper.SlideHelper; +import org.apache.poi.sl.usermodel.TextParagraph; +import org.apache.poi.xslf.usermodel.XSLFSlide; + +public class SlideLink extends Slide { + private final String link; + private final String caption; + private final byte[] resourceData; + private final String fileContentType; + + public SlideLink(String title, String description, String link, String caption, byte[] resourceData, String fileContentType) { + this.title = title; + this.description = description; + this.link = link; + this.caption = caption; + this.resourceData = resourceData; + this.fileContentType = fileContentType; + } + + @Override + public Object createApacheSlide(XSLFSlide newSlide) { + + SlideHelper.createTitle(newSlide, title, Slideshow.TITLE_HEIGHT, Slideshow.TITLE_FONT_SIZE, + TextParagraph.TextAlign.LEFT); + SlideHelper.createLink(newSlide, link); + SlideHelper.createImageWidthHeight(newSlide, resourceData, fileContentType, Slideshow.MAIN_CONTENT_MARGIN_TOP, + Slideshow.SVG_CONTENT_HEIGHT, Slideshow.SVG_CONTENT_WIDTH, true); + SlideHelper.createLegend(newSlide, caption); + + SlideHelper.addNotes(newSlide, description); + + return newSlide; + } +} diff --git a/backend/src/main/java/fr/cgi/magneto/model/slides/SlideMedia.java b/backend/src/main/java/fr/cgi/magneto/model/slides/SlideMedia.java new file mode 100644 index 000000000..fdce59e54 --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/model/slides/SlideMedia.java @@ -0,0 +1,66 @@ +package fr.cgi.magneto.model.slides; + +import fr.cgi.magneto.core.constants.Slideshow; +import fr.cgi.magneto.helper.SlideHelper; +import org.apache.poi.sl.usermodel.TextParagraph; +import org.apache.poi.xslf.usermodel.XSLFSlide; + +public class SlideMedia extends Slide { + + public enum MediaType { + IMAGE, + AUDIO, + VIDEO + } + + private byte[] resourceData; + private final String fileContentType; + private final String caption; + private final MediaType mediaType; + + public SlideMedia(String title, String caption, byte[] resourceData, + String fileContentType) { + this.title = title; + this.caption = caption; + this.resourceData = resourceData; + this.fileContentType = fileContentType; + this.mediaType = determineMediaType(fileContentType); + } + + private MediaType determineMediaType(String contentType) { + String type = contentType.toLowerCase(); + MediaType mediaType; + + if (type.startsWith(Slideshow.CONTENT_TYPE_AUDIO)) { + mediaType = MediaType.AUDIO; + } else if (type.startsWith(Slideshow.CONTENT_TYPE_VIDEO)) { + mediaType = MediaType.VIDEO; + } else { + mediaType = MediaType.IMAGE; + } + return mediaType; + } + + @Override + public Object createApacheSlide(XSLFSlide newSlide) { + + SlideHelper.createTitle(newSlide, title, Slideshow.TITLE_HEIGHT, Slideshow.TITLE_FONT_SIZE, + TextParagraph.TextAlign.LEFT); + switch (mediaType) { + case AUDIO: + SlideHelper.createMedia(newSlide, resourceData, fileContentType, SlideMedia.MediaType.AUDIO); + break; + case VIDEO: + SlideHelper.createMedia(newSlide, resourceData, fileContentType, SlideMedia.MediaType.VIDEO); + break; + default: + SlideHelper.createImage(newSlide, resourceData, fileContentType, Slideshow.CONTENT_MARGIN_TOP, + Slideshow.IMAGE_CONTENT_HEIGHT, false); + } + SlideHelper.createLegend(newSlide, caption); + + SlideHelper.addNotes(newSlide, description); + + return newSlide; + } +} \ No newline at end of file diff --git a/backend/src/main/java/fr/cgi/magneto/model/slides/SlideText.java b/backend/src/main/java/fr/cgi/magneto/model/slides/SlideText.java new file mode 100644 index 000000000..4469ca1d6 --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/model/slides/SlideText.java @@ -0,0 +1,173 @@ +package fr.cgi.magneto.model.slides; + +import fr.cgi.magneto.core.constants.Slideshow; +import fr.cgi.magneto.helper.SlideHelper; +import org.apache.poi.sl.usermodel.AutoNumberingScheme; +import org.apache.poi.sl.usermodel.TextParagraph; +import org.apache.poi.xslf.usermodel.XSLFSlide; +import org.apache.poi.xslf.usermodel.XSLFTextBox; +import org.apache.poi.xslf.usermodel.XSLFTextParagraph; +import org.apache.poi.xslf.usermodel.XSLFTextRun; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; + +import java.awt.*; + +public class SlideText extends Slide { + + public SlideText(String title, String description) { + this.title = title; + this.description = description; + } + + @Override + public Object createApacheSlide(XSLFSlide newSlide) { + + SlideHelper.createTitle(newSlide, title, Slideshow.TITLE_HEIGHT, Slideshow.TITLE_FONT_SIZE, + TextParagraph.TextAlign.LEFT); + XSLFTextBox contentBox = SlideHelper.createContent(newSlide); + + Document doc = Jsoup.parse(description); + processHtmlContent(contentBox, doc.body()); + + return newSlide; + } + + private static boolean isBodyEmptyOrContainsEmptyParagraph(Element body) { + // Vérifier si le body est vide + if (body.children().isEmpty()) { + return true; + } + + // Vérifier si le body ne contient qu'une balise

vide + if (body.children().size() == 1) { + Element child = body.child(0); + if (child.tagName().equals("p") && child.html().trim().isEmpty()) { + return true; + } + } + + return false; + } + + public static boolean isDescriptionEmptyOrContainsEmptyParagraph(String description) { + return isBodyEmptyOrContainsEmptyParagraph(Jsoup.parse(description).body()); + } + + private void processHtmlContent(XSLFTextBox textBox, Element element) { + if (textBox.getTextParagraphs().isEmpty()) { + textBox.addNewTextParagraph(); + } + + if (!isBodyEmptyOrContainsEmptyParagraph(element)) { + for (Node node : element.childNodes()) { + if (node instanceof Element) { + Element elem = (Element) node; + XSLFTextParagraph para = textBox.addNewTextParagraph(); + XSLFTextRun run = para.addNewTextRun(); + + processStyle(elem, para, run); + + String text = elem.text().trim(); + if (!text.isEmpty()) { + run.setText(text); + } + } + } + } + } + + private void processInlineStyles(XSLFTextRun run, String style) { + String[] styles = style.split(";"); + for (String s : styles) { + String[] parts = s.split(":"); + if (parts.length == 2) { + String property = parts[0].trim(); + String value = parts[1].trim(); + + switch (property) { + case Slideshow.CSS_COLOR: + try { + run.setFontColor(Color.decode(value)); + } catch (NumberFormatException ignored) { + } + break; + case Slideshow.CSS_FONT_SIZE: + try { + String size = value.replaceAll("[^0-9]", ""); + run.setFontSize(Double.parseDouble(size)); + } catch (NumberFormatException ignored) { + } + break; + case Slideshow.CSS_TEXT_DECORATION: + if (value.contains(Slideshow.VALUE_UNDERLINE)) { + run.setUnderlined(true); + } + break; + case Slideshow.CSS_FONT_WEIGHT: + if (value.equals(Slideshow.VALUE_BOLD) || value.equals(Slideshow.VALUE_BOLD_WEIGHT)) { + run.setBold(true); + } + break; + case Slideshow.CSS_FONT_STYLE: + if (value.equals(Slideshow.VALUE_ITALIC)) { + run.setItalic(true); + } + break; + } + } + } + } + + private void processStyle(Element elem, XSLFTextParagraph para, XSLFTextRun run) { + switch (elem.tagName().toLowerCase()) { + case Slideshow.TAG_H1: + run.setFontSize(Slideshow.H1_FONT_SIZE); + run.setBold(true); + para.setIndentLevel(Slideshow.H1_INDENT_LEVEL); + break; + case Slideshow.TAG_H2: + run.setFontSize(Slideshow.H2_FONT_SIZE); + run.setBold(true); + para.setIndentLevel(Slideshow.H2_INDENT_LEVEL); + break; + case Slideshow.TAG_H3: + run.setFontSize(Slideshow.H3_FONT_SIZE); + run.setBold(true); + para.setIndentLevel(Slideshow.H3_INDENT_LEVEL); + break; + case Slideshow.TAG_BOLD: + case Slideshow.TAG_STRONG: + run.setBold(true); + break; + case Slideshow.TAG_ITALIC: + case Slideshow.TAG_EM: + run.setItalic(true); + break; + case Slideshow.TAG_UNDERLINE: + run.setUnderlined(true); + break; + case Slideshow.TAG_PARAGRAPH: + para.setSpaceBefore(Slideshow.PARAGRAPH_SPACE_BEFORE); + para.setSpaceAfter(Slideshow.PARAGRAPH_SPACE_AFTER); + break; + case Slideshow.TAG_UNORDERED_LIST: + para.setIndentLevel(Slideshow.LIST_INDENT_LEVEL); + para.setBullet(true); + break; + case Slideshow.TAG_ORDERED_LIST: + para.setIndentLevel(Slideshow.LIST_INDENT_LEVEL); + para.setBulletAutoNumber(AutoNumberingScheme.arabicPeriod, 1); + break; + case Slideshow.TAG_LIST_ITEM: + para.setIndentLevel(Slideshow.LIST_INDENT_LEVEL); + break; + } + String style = elem.attr(Slideshow.CSS_STYLE); + if (!style.isEmpty()) { + processInlineStyles(run, style); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/fr/cgi/magneto/model/slides/SlideTitle.java b/backend/src/main/java/fr/cgi/magneto/model/slides/SlideTitle.java new file mode 100644 index 000000000..d3bcc84e0 --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/model/slides/SlideTitle.java @@ -0,0 +1,55 @@ +package fr.cgi.magneto.model.slides; + +import fr.cgi.magneto.core.constants.Slideshow; +import fr.cgi.magneto.helper.SlideHelper; +import org.apache.poi.sl.usermodel.TextParagraph; +import org.apache.poi.xslf.usermodel.XSLFSlide; +import org.apache.poi.xslf.usermodel.XSLFTextBox; +import org.apache.poi.xslf.usermodel.XSLFTextParagraph; +import org.apache.poi.xslf.usermodel.XSLFTextRun; + +public class SlideTitle extends Slide { + private final String title; + private final String description; + private final String ownerName; + private final String modificationDate; + private final byte[] resourceData; + private final String contentType; + + public SlideTitle(String title, String description, String ownerName, String modificationDate, byte[] resourceData, + String contentType) { + this.title = title; + this.description = description; + this.ownerName = ownerName; + this.modificationDate = modificationDate; + this.resourceData = resourceData; + this.contentType = contentType; + } + + @Override + public Object createApacheSlide(XSLFSlide newSlide) { + // TITRE + SlideHelper.createTitle(newSlide, title, Slideshow.MAIN_TITLE_HEIGHT, + Slideshow.MAIN_TITLE_FONT_SIZE, TextParagraph.TextAlign.CENTER); + + XSLFTextBox textBox = SlideHelper.createContent(newSlide); + + XSLFTextParagraph paragraph = textBox.addNewTextParagraph(); + paragraph.setTextAlign(TextParagraph.TextAlign.CENTER); + XSLFTextRun textRun = paragraph.addNewTextRun(); + textRun.setText(ownerName); + textRun.setFontSize(Slideshow.CONTENT_FONT_SIZE); + + XSLFTextParagraph paragraph2 = textBox.addNewTextParagraph(); + paragraph2.setTextAlign(TextParagraph.TextAlign.CENTER); + XSLFTextRun textRun2 = paragraph2.addNewTextRun(); + textRun2.setText(modificationDate); + textRun2.setFontSize(Slideshow.CONTENT_FONT_SIZE); + + SlideHelper.createImage(newSlide, resourceData, contentType, + Slideshow.MAIN_CONTENT_MARGIN_TOP, Slideshow.MAIN_IMAGE_CONTENT_HEIGHT, false); + + return newSlide; + + } +} \ No newline at end of file diff --git a/backend/src/main/java/fr/cgi/magneto/service/CardService.java b/backend/src/main/java/fr/cgi/magneto/service/CardService.java index 9272457b0..40bec09e6 100644 --- a/backend/src/main/java/fr/cgi/magneto/service/CardService.java +++ b/backend/src/main/java/fr/cgi/magneto/service/CardService.java @@ -146,6 +146,16 @@ void removeCardSectionWithLocked(CardPayload updateCard, String oldBoardId, Futu */ Future getAllCardsBySection(Section section, Integer page, UserInfos user); + /** + * Get all cards by section, without the count and returning the list directly + * + * @param section Section object + * @param page Page number + * @param user {@link UserInfos} User info + * @return Future {@link Future >} containing the cards corresponding to the board identifier + */ + Future> getAllCardsBySectionSimple(Section section, Integer page, UserInfos user); + /** * Duplicate cards * diff --git a/backend/src/main/java/fr/cgi/magneto/service/ExportService.java b/backend/src/main/java/fr/cgi/magneto/service/ExportService.java new file mode 100644 index 000000000..06bc995fa --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/service/ExportService.java @@ -0,0 +1,18 @@ +package fr.cgi.magneto.service; + +import fr.cgi.magneto.helper.I18nHelper; +import io.vertx.core.Future; +import org.entcore.common.user.UserInfos; + +import java.io.ByteArrayOutputStream; + +public interface ExportService { + + /** + * Export board to PPTX + * + * @param boardId Board identifier + * @return Future {@link Future} containing board id + */ + Future exportBoardToArchive(String boardId, UserInfos user, I18nHelper i18nHelper); +} diff --git a/backend/src/main/java/fr/cgi/magneto/service/SectionService.java b/backend/src/main/java/fr/cgi/magneto/service/SectionService.java index 4a9b036a8..9b241baf8 100644 --- a/backend/src/main/java/fr/cgi/magneto/service/SectionService.java +++ b/backend/src/main/java/fr/cgi/magneto/service/SectionService.java @@ -28,9 +28,10 @@ public interface SectionService { */ Future> getSectionsByBoardId(String boardId); + Future> createSectionWithCards(Board board, UserInfos user); + /** - * - * @param board board of the sections + * @param board board of the sections * @param isReadOnly * @return */ diff --git a/backend/src/main/java/fr/cgi/magneto/service/ServiceFactory.java b/backend/src/main/java/fr/cgi/magneto/service/ServiceFactory.java index e6accc5cf..34f810f22 100644 --- a/backend/src/main/java/fr/cgi/magneto/service/ServiceFactory.java +++ b/backend/src/main/java/fr/cgi/magneto/service/ServiceFactory.java @@ -30,6 +30,7 @@ public class ServiceFactory { private final FolderService folderService; private final BoardService boardService; private final CardService cardService; + private final ExportService exportService; private final Map securedActions; private final ShareNormalizer shareNormalizer; @@ -46,6 +47,7 @@ public ServiceFactory(Vertx vertx, Storage storage, MagnetoConfig magnetoConfig, this.folderService = new DefaultFolderService(CollectionsConstant.FOLDER_COLLECTION, mongoDb, this); this.cardService = new DefaultCardService(CollectionsConstant.CARD_COLLECTION, mongoDb, this); this.boardService = new DefaultBoardService(CollectionsConstant.BOARD_COLLECTION, mongoDb, this); + this.exportService = new DefaultExportService(this); } public MagnetoService magnetoServiceExample() { @@ -79,6 +81,11 @@ public SectionService sectionService() { public FolderService folderService() { return this.folderService; } + + public ExportService exportService() { + return this.exportService; + } + public WorkspaceService workSpaceService() { return new DefaultWorkspaceService(vertx, mongoDb); } diff --git a/backend/src/main/java/fr/cgi/magneto/service/impl/DefaultCardService.java b/backend/src/main/java/fr/cgi/magneto/service/impl/DefaultCardService.java index 203a42e28..bc76cede3 100644 --- a/backend/src/main/java/fr/cgi/magneto/service/impl/DefaultCardService.java +++ b/backend/src/main/java/fr/cgi/magneto/service/impl/DefaultCardService.java @@ -811,6 +811,22 @@ public Future getAllCardsBySection(Section section, Integer page, Us return promise.future(); } + @Override + public Future> getAllCardsBySectionSimple(Section section, Integer page, UserInfos user) { + Promise> promise = Promise.promise(); + + fetchAllCardsBySection(section, page, user) + .compose(this::setMetadataCards) + .onFailure(fail -> { + log.error("[Magneto@%s::getAllCardsBySectionSimple] Failed to get section cards", this.getClass().getSimpleName(), + fail.getMessage()); + promise.fail(fail.getMessage()); + }) + .onSuccess(promise::complete); + + return promise.future(); + } + public Future> getAllCardsByCreationDate(StatisticsPayload statisticsPayload) { Promise> promise = Promise.promise(); diff --git a/backend/src/main/java/fr/cgi/magneto/service/impl/DefaultExportService.java b/backend/src/main/java/fr/cgi/magneto/service/impl/DefaultExportService.java new file mode 100644 index 000000000..1ff896873 --- /dev/null +++ b/backend/src/main/java/fr/cgi/magneto/service/impl/DefaultExportService.java @@ -0,0 +1,595 @@ +package fr.cgi.magneto.service.impl; + +import fr.cgi.magneto.core.constants.CollectionsConstant; +import fr.cgi.magneto.core.constants.Field; +import fr.cgi.magneto.core.constants.MagnetoPaths; +import fr.cgi.magneto.core.constants.Slideshow; +import fr.cgi.magneto.core.enums.SlideResourceType; +import fr.cgi.magneto.factory.SlideFactory; +import fr.cgi.magneto.helper.I18nHelper; +import fr.cgi.magneto.helper.SlideHelper; +import fr.cgi.magneto.model.Section; +import fr.cgi.magneto.model.boards.Board; +import fr.cgi.magneto.model.cards.Card; +import fr.cgi.magneto.model.properties.SlideProperties; +import fr.cgi.magneto.model.slides.Slide; +import fr.cgi.magneto.service.ExportService; +import fr.cgi.magneto.service.ServiceFactory; +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.buffer.impl.BufferImpl; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.io.IOUtils; +import org.apache.poi.sl.usermodel.TextParagraph; +import org.apache.poi.xslf.usermodel.XMLSlideShow; +import org.apache.poi.xslf.usermodel.XSLFSlide; +import org.entcore.common.user.UserInfos; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static fr.cgi.magneto.core.enums.FileFormatManager.loadResourceForExtension; +import static fr.cgi.magneto.helper.SlideHelper.generateUniqueFileName; +import static fr.cgi.magneto.model.slides.SlideText.isDescriptionEmptyOrContainsEmptyParagraph; + +public class DefaultExportService implements ExportService { + + protected static final Logger log = LoggerFactory.getLogger(DefaultExportService.class); + private final ServiceFactory serviceFactory; + + public DefaultExportService(ServiceFactory serviceFactory) { + this.serviceFactory = serviceFactory; + } + + @Override + public Future exportBoardToArchive(String boardId, UserInfos user, I18nHelper i18nHelper) { + Promise promise = Promise.promise(); + + serviceFactory.boardService().getBoards(Collections.singletonList(boardId)) + .compose(boards -> { + if (boards.isEmpty()) { + String message = String.format("[Magneto@%s::exportBoardToArchive] No board found with id %s", + this.getClass().getSimpleName(), boardId); + log.error(message, new Throwable(message)); + return Future.failedFuture(message); + } + Board board = boards.get(0); + JsonObject slideShow = createSlideShowObject(board); + + List> documents = new ArrayList<>(); + + // D'abord on récupère les documents + return serviceFactory.boardService() + .getAllDocumentIds(boardId, user) + .compose(documentIds -> { + String imageUrl = board.getImageUrl(); + String imageId = imageUrl.substring(imageUrl.lastIndexOf('/') + 1); + documentIds.add(imageId); + return getBoardDocuments(documentIds); + }) + .compose(docs -> { + documents.addAll(docs); + return board.isLayoutFree() + ? createFreeLayoutSlideObjects(board, user, slideShow, documents, i18nHelper) + : createSectionLayoutSlideObjects(board, user, slideShow, documents, i18nHelper); + }) + .compose(pptx -> { + try { + // Créer l'archive ZIP + ByteArrayOutputStream archiveOutputStream = new ByteArrayOutputStream(); + ZipOutputStream zipOutputStream = new ZipOutputStream(archiveOutputStream); + + // Ajouter le PPTX à la racine de l'archive + ZipEntry pptxEntry = new ZipEntry(board.getTitle() + ".pptx"); + zipOutputStream.putNextEntry(pptxEntry); + ByteArrayOutputStream pptxOutputStream = new ByteArrayOutputStream(); + pptx.write(pptxOutputStream); + zipOutputStream.write(pptxOutputStream.toByteArray()); + zipOutputStream.closeEntry(); + + // Ajouter chaque document dans le dossier "Fichiers Liés" + Set usedFileNames = new HashSet<>(); + //On filtre les documents en double + List> uniqueDocuments = new ArrayList<>(documents.stream() + .collect(Collectors.toMap( + doc -> (String) doc.get(Field.FILENAME), // clé + doc -> doc, // valeur + (doc1, doc2) -> doc1 // en cas de doublon, garde le premier + )) + .values()); + for (Map doc : uniqueDocuments) { + // Déterminer l'extension de fichier basée sur le contentType + String originalFileName = (String) doc.get(Field.FILENAME); + String uniqueFileName = generateUniqueFileName(usedFileNames, originalFileName); + String fullPath = "Fichiers Liés/" + uniqueFileName; + + ZipEntry docEntry = new ZipEntry(fullPath); + zipOutputStream.putNextEntry(docEntry); + + BufferImpl buffer = (BufferImpl) doc.get(Field.BUFFER); + byte[] bytes = buffer.getBytes(); + + zipOutputStream.write(bytes); + zipOutputStream.closeEntry(); + } + + zipOutputStream.close(); + return Future.succeededFuture(archiveOutputStream); + } catch (Exception e) { + String message = String.format("[Magneto@%s::exportBoardToArchive] Failed to create archive: %s", + this.getClass().getSimpleName(), e.getMessage()); + log.error(message, e); + return Future.failedFuture(message); + } + }) + .onSuccess(promise::complete) + .onFailure(err -> { + String message = String.format( + "[Magneto@%s::exportBoardToArchive] Failed to get documents: %s", + this.getClass().getSimpleName(), err.getMessage()); + log.error(message); + promise.fail(message); + }); + }) + .onFailure(err -> { + String message = String.format("[Magneto@%s::exportBoardToArchive] Failed to export board: %s", + this.getClass().getSimpleName(), err.getMessage()); + log.error(message); + promise.fail(message); + }); + + return promise.future(); + } + + private Future fetchDocumentFile(String documentId, List> documents) { + return serviceFactory.workSpaceService().getDocument(documentId) + .compose(document -> { + String fileId = document.getString(Field.FILE); + if (fileId == null) { + log.warn("File ID is null for document: " + documentId); + return Future.succeededFuture(); + } + + JsonObject metadata = document.getJsonObject(Field.METADATA, new JsonObject()); + return Future.future(promise -> { + serviceFactory.storage().readFile(fileId, buffer -> { + if (buffer != null) { + Map docInfo = new HashMap<>(); + docInfo.put(Field.DOCUMENTID, documentId); + docInfo.put(Field.BUFFER, buffer); + docInfo.put(Field.CONTENTTYPE, metadata.getString(Field.CONTENT_TYPE, "")); + docInfo.put(Field.FILENAME, metadata.getString(Field.FILENAME, "")); + documents.add(docInfo); + promise.complete(); + } else { + log.warn("Could not read file for document: " + documentId); + promise.complete(); + } + }); + }); + }) + .recover(err -> { + log.warn("Error processing document " + documentId + ": " + err.getMessage()); + return Future.succeededFuture(); + }); + } + + private JsonObject createSlideShowObject(Board board) { + if (board == null) { + log.error("[Magneto@%s::createSlideShowObject] Board is null", this.getClass().getSimpleName()); + return new JsonObject(); + } + + return new JsonObject() + .put(Field.TITLE, board.getTitle()) + .put(Field.DESCRIPTION, board.getDescription()) + .put(Field.OWNERNAME, board.getOwnerName()) + .put(Field.MODIFICATIONDATE, board.getModificationDate()) + .put(Field.SHARED, board.getShared() != null && !board.getShared().isEmpty()) + .put(Field.MAGNET_NUMBER, board.isLayoutFree() ? board.getNbCards() : board.getNbCardsSections()) + .put(Field.ISPUBLIC, board.isPublic()) + .put(Field.SLIDE_OBJECTS, new JsonArray()); + } + + private Future>> getBoardDocuments(List documentIds) { + List> documents = new ArrayList<>(); + List futures = new ArrayList<>(); + + for (String documentId : documentIds) { + Future future = fetchDocumentFile(documentId, documents); + futures.add(future); + } + + return CompositeFuture.all(futures) + .map(v -> documents) + .otherwiseEmpty(); + } + + private Future createFreeLayoutSlideObjects(Board board, UserInfos user, + JsonObject slideShowData, List> documents, I18nHelper i18nHelper) { + XMLSlideShow ppt = new XMLSlideShow(); + ppt.setPageSize(new java.awt.Dimension(1280, 720)); + + SlideFactory slideFactory = new SlideFactory(); + + // TITRE + Slide titleSlide = createTitleSlide(board, slideFactory, documents, i18nHelper); + XSLFSlide newTitleSlide = ppt.createSlide(); + titleSlide.createApacheSlide(newTitleSlide); + + // DESCRIPTION SI NON VIDE + if (!isDescriptionEmptyOrContainsEmptyParagraph(board.getDescription())){ + Slide descriptionSlide = createDescriptionSlide(board, slideFactory, i18nHelper); + XSLFSlide newDescriptionSlide = ppt.createSlide(); + descriptionSlide.createApacheSlide(newDescriptionSlide); + } + + return serviceFactory.cardService().getAllCardsByBoard(board, user) + .compose(fetchedCards -> { + Map cardMap = fetchedCards.stream() + .collect(Collectors.toMap(Card::getId, card -> card)); + + // Créer un Future initial qui réussit immédiatement + Future cardProcessingFuture = Future.succeededFuture(); + + // Traiter les cartes en séquence en chaînant les Futures + for (Card boardCard : board.cards()) { + String cardId = boardCard.getId(); + Card card = cardMap.get(cardId); + + if (card != null) { + // Capture la variable pour l'utiliser dans la lambda + final Card finalCard = card; + + // Ajouter cette carte à la chaîne de traitements + cardProcessingFuture = cardProcessingFuture.compose(v -> { + try { + return processCardResourceType(finalCard, slideFactory, slideShowData, documents, + ppt, i18nHelper); + } catch (Exception e) { + log.error("Failed to process card: " + finalCard.getId(), e); + return Future.succeededFuture(); // Continue avec la prochaine carte + } + }); + } + } + + // Retourner le ppt une fois que toutes les cartes ont été traitées + return cardProcessingFuture.map(v -> ppt); + }); + } + + private Future createSectionLayoutSlideObjects(Board board, UserInfos user, + JsonObject slideShowData, List> documents, I18nHelper i18nHelper) { + XMLSlideShow ppt = new XMLSlideShow(); + ppt.setPageSize(new java.awt.Dimension(1280, 720)); + + SlideFactory slideFactory = new SlideFactory(); + + // TITRE + Slide titleSlide = createTitleSlide(board, slideFactory, documents, i18nHelper); + XSLFSlide newTitleSlide = ppt.createSlide(); + titleSlide.createApacheSlide(newTitleSlide); + + // DESCRIPTION SI NON VIDE + if (!isDescriptionEmptyOrContainsEmptyParagraph(board.getDescription())){ + Slide descriptionSlide = createDescriptionSlide(board, slideFactory, i18nHelper); + XSLFSlide newDescriptionSlide = ppt.createSlide(); + descriptionSlide.createApacheSlide(newDescriptionSlide); + } + + return this.serviceFactory.sectionService().createSectionWithCards(board, user) + .compose(sections -> { + // Créer un Future initial qui réussit immédiatement + Future processingFuture = Future.succeededFuture(ppt); + + // Traiter chaque section et ses cartes séquentiellement + for (Section section : sections) { + processingFuture = processingFuture.compose(currentPpt -> { + // TITRE SECTION + XSLFSlide sectionApacheSlide = currentPpt.createSlide(); + SlideHelper.createTitle(sectionApacheSlide, section.getTitle(), + Slideshow.MAIN_TITLE_HEIGHT, + Slideshow.MAIN_TITLE_FONT_SIZE, + TextParagraph.TextAlign.CENTER); + + // Future pour le traitement séquentiel des cartes de cette section + Future sectionFuture = Future.succeededFuture(currentPpt); + + for (Card card : section.getCards()) { + if (card != null) { + final Card finalCard = card; + sectionFuture = sectionFuture.compose(pptInProgress -> + processCardResourceType(finalCard, slideFactory, slideShowData, + documents, pptInProgress, i18nHelper) + .map(v -> pptInProgress) // Retourne toujours la présentation + .recover(err -> { + log.error("Failed to process card: " + finalCard.getId(), err); + return Future.succeededFuture(pptInProgress); // Continue avec la prochaine carte + }) + ); + } + } + + return sectionFuture; + }); + } + + return processingFuture; + }) + .onFailure(err -> { + String message = String.format( + "[Magneto@%s::createSectionLayoutSlideObjects] Failed to create slides: %s", + this.getClass().getSimpleName(), err.getMessage()); + log.error(message); + }); + } + + private Slide createSlideFromCard(Card card, SlideFactory slideFactory, + List> documents, JsonObject referencedBoardData, I18nHelper i18nHelper) { + SlideProperties.Builder propertiesBuilder = new SlideProperties.Builder() + .title(card.getTitle()) + .description(card.getDescription()); + + SlideResourceType resourceType = SlideResourceType.fromString(card.getResourceType()); + switch (resourceType) { + case DEFAULT: + case TEXT: + break; + case FILE: + case PDF: + case SHEET: + try { + String svgSource = loadResourceForExtension(card.getMetadata().getExtension()); + ClassLoader classLoader = getClass().getClassLoader(); + InputStream inputStream = classLoader.getResourceAsStream(svgSource); + + if (inputStream != null) { + byte[] svgData = IOUtils.toByteArray(inputStream); + String fileNameString = i18nHelper.translate(CollectionsConstant.I18N_SLIDESHOW_FILENAME) + + (card.getMetadata() != null ? card.getMetadata().getFilename() : "") + + "\nLe fichier est disponible dans le dossier « Fichiers liés »."; + propertiesBuilder + .fileNameString(fileNameString) + .caption(card.getCaption()) + .resourceData(svgData) + .contentType("image/svg+xml"); + } else { + log.warn("SVG file not found in resources"); + // Traitement alternatif si le fichier n'est pas trouvé + } + } catch (IOException e) { + log.error("Failed to load SVG file", e); + } + break; + case LINK: + case HYPERLINK: + case EMBEDDER: + buildLink(card, propertiesBuilder); + break; + case IMAGE: + case VIDEO: + case AUDIO: + //EMBEDDED LINK + if (card.getResourceId() == null || card.getResourceId().isEmpty()){ + card.setResourceType(SlideResourceType.EMBEDDER.getValue()); + resourceType = SlideResourceType.fromString(card.getResourceType()); + buildLink(card, propertiesBuilder); + } + else { + //MEDIA + Map> documentMap = new HashMap<>(); + for (Map doc : documents) { + documentMap.put((String) doc.get(Field.DOCUMENTID), doc); + } + Map documentData = documentMap.get(card.getResourceId()); + + Buffer documentBuffer = documentData != null ? (Buffer) documentData.get(Field.BUFFER) : null; + String contentType = documentData != null ? (String) documentData.get(Field.CONTENTTYPE) : ""; + + propertiesBuilder + .contentType(contentType) + .resourceData(documentBuffer != null ? documentBuffer.getBytes() : null) + .caption(card.getCaption()); + } + break; + case BOARD: + if (referencedBoardData != null) { + JsonObject i18nValues = new JsonObject() + .put(CollectionsConstant.I18N_SLIDESHOW_OWNER, + i18nHelper.translate(CollectionsConstant.I18N_SLIDESHOW_OWNER)) + .put(CollectionsConstant.I18N_SLIDESHOW_UPDATED, + i18nHelper.translate(CollectionsConstant.I18N_SLIDESHOW_UPDATED)) + .put(CollectionsConstant.I18N_SLIDESHOW_MAGNETS, + i18nHelper.translate(CollectionsConstant.I18N_SLIDESHOW_MAGNETS)) + .put(CollectionsConstant.I18N_SLIDESHOW_SHARED, + i18nHelper.translate(CollectionsConstant.I18N_SLIDESHOW_SHARED)) + .put(CollectionsConstant.I18N_SLIDESHOW_PLATFORM, + i18nHelper.translate(CollectionsConstant.I18N_SLIDESHOW_PLATFORM)); + + String boardImageId = referencedBoardData.getString(Field.BOARD_IMAGE_ID, ""); + + // Créer une map pour un accès rapide aux documents par ID + Map> boardDocumentMap = new HashMap<>(); + for (Map doc : documents) { + boardDocumentMap.put((String) doc.get(Field.DOCUMENTID), doc); + } + + // Récupérer les données de l'image + Map imageDocumentData = boardDocumentMap.get(boardImageId); + Buffer imageBuffer = null; + String imageContentType = ""; + + if (imageDocumentData != null) { + imageBuffer = (Buffer) imageDocumentData.get(Field.BUFFER); + imageContentType = (String) imageDocumentData.get(Field.CONTENTTYPE); + } + + propertiesBuilder + .title(referencedBoardData.getString(Field.TITLE)) + .description(referencedBoardData.getString(Field.DESCRIPTION)) + .caption(card.getCaption()) + .ownerName(referencedBoardData.getString(Field.OWNERNAME)) + .modificationDate(referencedBoardData.getString(Field.MODIFICATIONDATE)) + .resourceNumber(referencedBoardData.getInteger(Field.MAGNET_NUMBER)) + .isShare(referencedBoardData.getBoolean(Field.SHARED)) + .link(processBoardUrl(card.getResourceUrl())) + .contentType(imageContentType) + .resourceData(imageBuffer != null ? imageBuffer.getBytes() : null) + .isPublic(referencedBoardData.getBoolean(Field.ISPUBLIC)) + .i18ns(i18nValues); + } + break; + } + + return slideFactory.createSlide(resourceType, propertiesBuilder.build()); + } + + private void buildLink(Card card, SlideProperties.Builder propertiesBuilder) { + try { + ClassLoader classLoader = getClass().getClassLoader(); + InputStream inputStream = classLoader.getResourceAsStream("img/extension/link.svg"); + + if (inputStream != null) { + byte[] svgData = IOUtils.toByteArray(inputStream); + + propertiesBuilder + .resourceUrl(card.getResourceUrl()) + .caption(card.getCaption()) + .resourceData(svgData) + .contentType("image/svg+xml"); + } else { + log.warn("SVG file not found in resources"); + // Traitement alternatif si le fichier n'est pas trouvé + } + } catch (IOException e) { + log.error("Failed to load SVG file", e); + } + } + + private Future processCardResourceType(Card card, SlideFactory slideFactory, JsonObject slideShowData, + List> documents, XMLSlideShow ppt, I18nHelper i18nHelper) { + if (SlideResourceType.BOARD.getValue().equals(card.getResourceType())) { + return serviceFactory.boardService() + .getBoards(Collections.singletonList(card.getResourceUrl())) + .compose(boards -> { + if (boards.isEmpty()) { + return Future.succeededFuture(); + } + + Board referencedBoard = boards.get(0); + JsonObject referencedSlideShow = createSlideShowObject(referencedBoard); + + String imageUrl = referencedBoard.getImageUrl(); + if (imageUrl == null || imageUrl.isEmpty()) { + Slide slide = createSlideFromCard(card, slideFactory, + documents, referencedSlideShow, i18nHelper); + XSLFSlide newSlide = ppt.createSlide(); + slide.createApacheSlide(newSlide); + return Future.succeededFuture(); + } + + String imageId = imageUrl.substring(imageUrl.lastIndexOf('/') + 1); + referencedSlideShow.put(Field.BOARD_IMAGE_ID, imageId); + + boolean imageExists = documents.stream() + .anyMatch(doc -> imageId.equals(doc.get(Field.DOCUMENTID))); + + if (!imageExists) { + return fetchDocumentFile(imageId, documents) + .compose(v -> { + Slide slide = createSlideFromCard(card, slideFactory, + documents, referencedSlideShow, i18nHelper); + XSLFSlide newSlide = ppt.createSlide(); + slide.createApacheSlide(newSlide); + return Future.succeededFuture(); + }); + } else { + Slide slide = createSlideFromCard(card, slideFactory, + documents, referencedSlideShow, i18nHelper); + XSLFSlide newSlide = ppt.createSlide(); + slide.createApacheSlide(newSlide); + return Future.succeededFuture(); + } + }); + } else { + Slide slide = createSlideFromCard(card, slideFactory, documents, null, i18nHelper); + XSLFSlide newSlide = ppt.createSlide(); + slide.createApacheSlide(newSlide); + return Future.succeededFuture(); + } + } + + private String processBoardUrl(String boardId) { + if (boardId == null || boardId.isEmpty()) { + return ""; + } + + String baseUrl = serviceFactory.magnetoConfig().host(); + + if (!baseUrl.endsWith("/")) { + baseUrl += "/"; + } + + return baseUrl + MagnetoPaths.MAGNETO_BOARD + boardId + MagnetoPaths.VIEW; + } + + private Slide createTitleSlide(Board board, SlideFactory slideFactory, List> documents, + I18nHelper i18nHelper) { + SlideProperties.Builder propertiesBuilder = new SlideProperties.Builder() + .title(board.getTitle()) + .description(board.getDescription()); + + String imageUrl = board.getImageUrl(); + String imageId = imageUrl.substring(imageUrl.lastIndexOf('/') + 1); + Map documentData = documents.stream() + .filter(doc -> imageId.equals(doc.get(Field.DOCUMENTID))) + .findFirst() + .orElse(null); + Buffer documentBuffer = (Buffer) documentData.get(Field.BUFFER); + String contentType = documentData != null ? (String) documentData.get(Field.CONTENTTYPE) : ""; + + // Format the modification date to dd/MM/yyyy + String formattedModificationDate = ""; + try { + // Parse the original date string + SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + // Format to the desired output + SimpleDateFormat outputFormat = new SimpleDateFormat("dd/MM/yyyy"); + + Date parsedDate = inputFormat.parse(board.getModificationDate()); + formattedModificationDate = outputFormat.format(parsedDate); + } catch (ParseException e) { + formattedModificationDate = board.getModificationDate(); + } + + propertiesBuilder + .ownerName(i18nHelper.translate("magneto.slideshow.created.by") + board.getOwnerName() + ",") + .modificationDate(i18nHelper.translate("magneto.slideshow.updated.the") + formattedModificationDate) + .resourceData(documentBuffer != null ? documentBuffer.getBytes() : null) + .contentType(contentType); + + return slideFactory.createSlide(SlideResourceType.TITLE, propertiesBuilder.build()); + } + + private Slide createDescriptionSlide(Board board, SlideFactory slideFactory, + I18nHelper i18nHelper) { + SlideProperties.Builder propertiesBuilder = new SlideProperties.Builder() + .title(i18nHelper.translate("magneto.create.board.description")) + .description(board.getDescription()); + + return slideFactory.createSlide(SlideResourceType.DESCRIPTION, propertiesBuilder.build()); + } +} \ No newline at end of file diff --git a/backend/src/main/java/fr/cgi/magneto/service/impl/DefaultSectionService.java b/backend/src/main/java/fr/cgi/magneto/service/impl/DefaultSectionService.java index 0f63a253b..c93495e56 100644 --- a/backend/src/main/java/fr/cgi/magneto/service/impl/DefaultSectionService.java +++ b/backend/src/main/java/fr/cgi/magneto/service/impl/DefaultSectionService.java @@ -78,6 +78,31 @@ public Future> getSectionsByBoardId(String boardId) { return promise.future(); } + @Override + public Future> createSectionWithCards(Board board, UserInfos user) { + Promise> promise = Promise.promise(); + + List
sections = new ArrayList<>(); + this.serviceFactory.sectionService().getSectionsByBoardId(board.getId()) + .compose(sectionsResult -> { + sections.addAll(sectionsResult); + List futures = new ArrayList<>(); + for (Section section : sections) { + Future> cardsFuture = this.serviceFactory.cardService().getAllCardsBySectionSimple(section, 0, user); + futures.add(cardsFuture.map(cards -> { + section.setCards(cards); + return section; + })); + } + return CompositeFuture.all(futures); + }) + .compose(compositeFuture -> Future.succeededFuture(sections)) + .onSuccess(promise::complete) + .onFailure(promise::fail); + + return promise.future(); + } + @Override public Future> getSectionsByBoard(Board board, boolean isReadOnly) { Promise> promise = Promise.promise(); diff --git a/backend/src/main/resources/i18n/de.json b/backend/src/main/resources/i18n/de.json index 7f8e54578..14e5435ff 100644 --- a/backend/src/main/resources/i18n/de.json +++ b/backend/src/main/resources/i18n/de.json @@ -205,6 +205,12 @@ "magneto.dropzone.overlay.solution": "Veuillez importer un fichier", "magneto.dropzone.overlay.action": "J'ai compris", "magneto.shared.push.notif.body": "shared a board with you", + "magneto.slideshow.created.by": "Créé par ", + "magneto.slideshow.updated.the": "mis à jour le ", + "magneto.slideshow.owner": "Propriétaire : ", + "magneto.slideshow.magnets": "aimants", + "magneto.slideshow.shared": "Tableau partagé", + "magneto.slideshow.platform": "Tableau de la plateforme", "magneto.create.board.options.display.favorites": "Afficher le nombre de favoris sur les aimants", "magneto.navbar.home": "Accueil", "magneto.navbar.documents": "Documents", diff --git a/backend/src/main/resources/i18n/en.json b/backend/src/main/resources/i18n/en.json index ae72caeae..fbab37aa6 100644 --- a/backend/src/main/resources/i18n/en.json +++ b/backend/src/main/resources/i18n/en.json @@ -205,6 +205,12 @@ "magneto.dropzone.overlay.solution": "Veuillez importer un fichier", "magneto.dropzone.overlay.action": "J'ai compris", "magneto.shared.push.notif.body": "shared a board with you", + "magneto.slideshow.created.by": "Créé par ", + "magneto.slideshow.updated.the": "mis à jour le ", + "magneto.slideshow.owner": "Propriétaire : ", + "magneto.slideshow.magnets": "aimants", + "magneto.slideshow.shared": "Tableau partagé", + "magneto.slideshow.platform": "Tableau de la plateforme", "magneto.create.board.options.display.favorites": "Afficher le nombre de favoris sur les aimants", "magneto.navbar.home": "Accueil", "magneto.navbar.documents": "Documents", diff --git a/backend/src/main/resources/i18n/es.json b/backend/src/main/resources/i18n/es.json index 461edcec6..e5174d678 100644 --- a/backend/src/main/resources/i18n/es.json +++ b/backend/src/main/resources/i18n/es.json @@ -203,6 +203,12 @@ "magneto.dropzone.overlay.solution": "Veuillez importer un fichier", "magneto.dropzone.overlay.action": "J'ai compris", "magneto.shared.push.notif.body": "shared a board with you", + "magneto.slideshow.created.by": "Créé par ", + "magneto.slideshow.updated.the": "mis à jour le ", + "magneto.slideshow.owner": "Propriétaire : ", + "magneto.slideshow.magnets": "aimants", + "magneto.slideshow.shared": "Tableau partagé", + "magneto.slideshow.platform": "Tableau de la plateforme", "magneto.create.board.options.display.favorites": "Afficher le nombre de favoris sur les aimants", "magneto.navbar.home": "Accueil", "magneto.navbar.documents": "Documents", diff --git a/backend/src/main/resources/i18n/fr.json b/backend/src/main/resources/i18n/fr.json index b35ffa578..a7a63acd1 100644 --- a/backend/src/main/resources/i18n/fr.json +++ b/backend/src/main/resources/i18n/fr.json @@ -58,6 +58,13 @@ "magneto.board.notify.text.2": "Une notification leur sera envoyée dans leur fil d'actualité leur indiquant que des modifications ont été faites sur ce tableau.", "magneto.board.notify.success": "Notification envoyée.", "magneto.board.notify.error": "Échec lors de l'envoi de la notification.", + "magneto.board.export": "Exporter le tableau", + "magneto.export": "Exporter", + "magneto.export.modal.format": "Exporter au format PowerPoint", + "magneto.export.modal.informations": "Informations concernant l'export", + "magneto.export.modal.content": "Une archive zip sera téléchargée. Elle contiendra le diaporama avec l’extension .pptx et un dossier avec les fichiers contenus dans le tableau.", + "magneto.export.modal.text.1": "Si vous exportez des aimants Texte, seul le contenu textuel au format brut apparaîtra dans la diapositive. Vous n'aurez plus qu'à faire la mise en page que vous souhaitez !", + "magneto.export.modal.text.2": "Les sections masquées ne seront pas incluses dans le diaporama.", "magneto.board.move.title": "Déplacer l'aimant sélectionné", "magneto.board.move.text": "Vers quel tableau souhaitez-vous déplacer cet aimant ?", "magneto.board.duplicate.move.title": "Dupliquer l'aimant sélectionné", @@ -224,6 +231,13 @@ "magneto.dropzone.overlay.solution": "Veuillez importer un fichier", "magneto.dropzone.overlay.action": "J'ai compris", "magneto.shared.push.notif.body": "a partagé avec vous un nouveau tableau", + "magneto.slideshow.created.by": "Créé par ", + "magneto.slideshow.updated.the": "mis à jour le ", + "magneto.slideshow.owner": "Propriétaire : ", + "magneto.slideshow.magnets": "aimants", + "magneto.slideshow.shared": "Tableau partagé", + "magneto.slideshow.platform": "Tableau de la plateforme", + "magneto.slideshow.filename": "Fichier : ", "magneto.notify.board.push.notif.body": "vous notifie de changements sur un tableau", "magneto.loading": "Chargement", "magneto.zoom": "Zoom", diff --git a/backend/src/main/resources/i18n/it.json b/backend/src/main/resources/i18n/it.json index d78b0b605..634946803 100644 --- a/backend/src/main/resources/i18n/it.json +++ b/backend/src/main/resources/i18n/it.json @@ -204,6 +204,12 @@ "magneto.dropzone.overlay.solution": "Veuillez importer un fichier", "magneto.dropzone.overlay.action": "J'ai compris", "magneto.shared.push.notif.body": "shared a board with you", + "magneto.slideshow.created.by": "Créé par ", + "magneto.slideshow.updated.the": "mis à jour le ", + "magneto.slideshow.owner": "Propriétaire : ", + "magneto.slideshow.magnets": "aimants", + "magneto.slideshow.shared": "Tableau partagé", + "magneto.slideshow.platform": "Tableau de la plateforme", "magneto.create.board.options.display.favorites": "Afficher le nombre de favoris sur les aimants", "magneto.navbar.home": "Accueil", "magneto.navbar.documents": "Documents", diff --git a/backend/src/main/resources/i18n/pt.json b/backend/src/main/resources/i18n/pt.json index 571f27adf..9a5c144eb 100644 --- a/backend/src/main/resources/i18n/pt.json +++ b/backend/src/main/resources/i18n/pt.json @@ -205,6 +205,12 @@ "magneto.dropzone.overlay.solution": "Veuillez importer un fichier", "magneto.dropzone.overlay.action": "J'ai compris", "magneto.shared.push.notif.body": "shared a board with you", + "magneto.slideshow.created.by": "Créé par ", + "magneto.slideshow.updated.the": "mis à jour le ", + "magneto.slideshow.owner": "Propriétaire : ", + "magneto.slideshow.magnets": "aimants", + "magneto.slideshow.shared": "Tableau partagé", + "magneto.slideshow.platform": "Tableau de la plateforme", "magneto.create.board.options.display.favorites": "Afficher le nombre de favoris sur les aimants", "magneto.navbar.home": "Accueil", "magneto.navbar.documents": "Documents", diff --git a/backend/src/main/resources/img/audio_icon.svg b/backend/src/main/resources/img/audio_icon.svg new file mode 100644 index 000000000..7a5ac7341 --- /dev/null +++ b/backend/src/main/resources/img/audio_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/backend/src/main/resources/img/extension/audio.svg b/backend/src/main/resources/img/extension/audio.svg new file mode 100644 index 000000000..f8e9c0d41 --- /dev/null +++ b/backend/src/main/resources/img/extension/audio.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/backend/src/main/resources/img/extension/default.svg b/backend/src/main/resources/img/extension/default.svg new file mode 100644 index 000000000..77e6ce67e --- /dev/null +++ b/backend/src/main/resources/img/extension/default.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/img/extension/file.svg b/backend/src/main/resources/img/extension/file.svg new file mode 100644 index 000000000..e570df03b --- /dev/null +++ b/backend/src/main/resources/img/extension/file.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/backend/src/main/resources/img/extension/image.svg b/backend/src/main/resources/img/extension/image.svg new file mode 100644 index 000000000..ee68b0baf --- /dev/null +++ b/backend/src/main/resources/img/extension/image.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/img/extension/link.svg b/backend/src/main/resources/img/extension/link.svg new file mode 100644 index 000000000..8932ee964 --- /dev/null +++ b/backend/src/main/resources/img/extension/link.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/img/extension/pdf.svg b/backend/src/main/resources/img/extension/pdf.svg new file mode 100644 index 000000000..cf2f1f27b --- /dev/null +++ b/backend/src/main/resources/img/extension/pdf.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/backend/src/main/resources/img/extension/sheet.svg b/backend/src/main/resources/img/extension/sheet.svg new file mode 100644 index 000000000..c15a8f956 --- /dev/null +++ b/backend/src/main/resources/img/extension/sheet.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/img/extension/text.svg b/backend/src/main/resources/img/extension/text.svg new file mode 100644 index 000000000..8e49234c5 --- /dev/null +++ b/backend/src/main/resources/img/extension/text.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/img/extension/video.svg b/backend/src/main/resources/img/extension/video.svg new file mode 100644 index 000000000..ec513965d --- /dev/null +++ b/backend/src/main/resources/img/extension/video.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/frontend/src/components/export-modal/ExportModal.tsx b/frontend/src/components/export-modal/ExportModal.tsx new file mode 100644 index 000000000..cec8a18db --- /dev/null +++ b/frontend/src/components/export-modal/ExportModal.tsx @@ -0,0 +1,195 @@ +import React, { useEffect, useState } from "react"; + +import { + Alert, + Button, + Box, + Tab, + Tabs, + Typography, + Modal, + IconButton, +} from "@cgi-learning-hub/ui"; +import CloseIcon from "@mui/icons-material/Close"; +import DownloadIcon from "@mui/icons-material/Download"; +import { Trans, useTranslation } from "react-i18next"; + +import { + alertListItemBulletStyle, + alertListItemContentStyle, + alertListItemStyle, + alertListStyle, + alertTitleStyle, + buttonStyle, + exportContentStyle, + exportTitleStyle, + tabsStyle, + tabStyle, +} from "./style"; +import { ExportModalProps } from "./types"; +import { + closeButtonStyle, + headerStyle, + modalContainerStyle, + modalFooterStyle, + titleStyle, +} from "../message-modal/style"; +import { useBoardsNavigation } from "~/providers/BoardsNavigationProvider"; +import { useExportBoardQuery } from "~/services/api/export.service.ts"; + +export const ExportModal: React.FunctionComponent = ({ + isOpen, + onClose, +}) => { + const { t } = useTranslation("magneto"); + const [tabValue] = useState(0); + const { selectedBoardsIds, selectedBoards } = useBoardsNavigation(); + + const [shouldFetch, setShouldFetch] = useState(false); + + const { data, error, isLoading } = useExportBoardQuery(selectedBoardsIds[0], { + skip: !shouldFetch, + }); + const handleExport = () => { + setShouldFetch(true); + }; + + useEffect(() => { + if (data) { + const blob = new Blob([data], { + type: "application/zip", + }); + + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", `${selectedBoards[0]._title}.zip`); + document.body.appendChild(link); + link.click(); + + link.parentNode?.removeChild(link); + window.URL.revokeObjectURL(url); + + setShouldFetch(false); + onClose(); + } + }, [data]); + + // Gestion des erreurs + useEffect(() => { + if (error) { + console.error("Erreur lors de l'export:", error); + setShouldFetch(false); + } + }, [error]); + + return ( + + + + + {t("magneto.board.export")} + + + + + + + + console.log("to be")} + variant="scrollable" + scrollButtons="false" + > + } + sx={tabStyle} + /> + + + {tabValue === 0 && ( + + + {t("magneto.export.modal.format")} + + + {t("magneto.export.modal.content")} + + + + {t("magneto.export.modal.informations")} + + + + + + , + }} + /> + + + + + + , + }} + /> + + + + + + )} + + + + + + + + + + ); +}; diff --git a/frontend/src/components/export-modal/style.ts b/frontend/src/components/export-modal/style.ts new file mode 100644 index 000000000..5e5e45a3f --- /dev/null +++ b/frontend/src/components/export-modal/style.ts @@ -0,0 +1,74 @@ +export const tabsStyle = { + borderBottom: 1, + borderColor: "divider", + width: "100%", + height: "65px", + overflow: "hidden", +}; + +// New style constants +export const tabStyle = { + display: "flex", + flexDirection: "row", + alignItems: "center", + maxHeight: "50px", + paddingBottom: 0, + fontSize: "1.6rem", + lineHeight: 2.4, + "& .MuiTab-iconWrapper": { + display: "flex", + alignItems: "center", + marginRight: "8px", + marginBottom: 0, + }, + "& .MuiTab-label": { + display: "flex", + alignItems: "center", + }, +}; + +export const exportTitleStyle = { + marginY: "1.5rem", + fontWeight: 500, + color: "#333333", + fontSize: "1.8rem", + font: "Comfortaa", +}; + +export const exportContentStyle = { + marginY: "1.5rem", + color: "text.primary", + fontSize: "1.6rem", +}; + +export const alertTitleStyle = { + fontWeight: 550, + marginBottom: "0.8rem", + fontSize: "1.6rem", + color: "#545F66", +}; + +export const alertListStyle = { + paddingLeft: "2rem", + margin: 0, + fontSize: "1.4rem", + color: "text.primary", + fontWeight: 400, +}; + +export const alertListItemStyle = { + marginBottom: "8px", + display: "flex", + alignItems: "flex-start", +}; + +export const alertListItemBulletStyle = { + marginRight: "8px", + lineHeight: 1.5, +}; + +export const alertListItemContentStyle = {}; + +export const buttonStyle = { + fontSize: "1.4rem", +}; diff --git a/frontend/src/components/export-modal/types.ts b/frontend/src/components/export-modal/types.ts new file mode 100644 index 000000000..507f2bd22 --- /dev/null +++ b/frontend/src/components/export-modal/types.ts @@ -0,0 +1,4 @@ +export type ExportModalProps = { + isOpen: boolean; + onClose: () => void; +}; diff --git a/frontend/src/components/read-view/ReadView.tsx b/frontend/src/components/read-view/ReadView.tsx index a0afd3d8d..5a7391250 100644 --- a/frontend/src/components/read-view/ReadView.tsx +++ b/frontend/src/components/read-view/ReadView.tsx @@ -90,7 +90,7 @@ export const ReadView: FC = () => { }, [board]); useEffect(() => { - if (!card) setCard(initialCards[0]); + if (!card) return setCard(initialCards[0]); }, [board]); useEffect(() => { diff --git a/frontend/src/components/toaster-container/ToasterContainer.tsx b/frontend/src/components/toaster-container/ToasterContainer.tsx index b2bd116e9..5055f48df 100644 --- a/frontend/src/components/toaster-container/ToasterContainer.tsx +++ b/frontend/src/components/toaster-container/ToasterContainer.tsx @@ -15,6 +15,7 @@ import { useNavigate } from "react-router-dom"; import { BoardPublicShareModal } from "../board-public-share-modal/BoardPublicShareModal"; import { CreateFolder } from "../create-folder/CreateFolder"; import { DeleteModal } from "../delete-modal/DeleteModal"; +import { ExportModal } from "../export-modal/ExportModal"; import { MessageModal } from "../message-modal/MessageModal"; import { MoveBoard } from "../move-board/MoveBoard"; import { ShareModalMagneto } from "../share-modal/ShareModalMagneto"; @@ -55,6 +56,7 @@ export const ToasterContainer = ({ const [isCreateOpen, toggleCreate] = useToggle(false); const [isNotifyOpen, toggleNotify] = useToggle(false); const [isMoveOpen, toggleMove] = useToggle(false); + const [isExportOpen, toggleExport] = useToggle(false); const [isMoveDelete, toggleDelete] = useToggle(false); const [isCreateFolder, toggleCreateFolder] = useToggle(false); const [isShareFolder, toggleShareFolder] = useToggle(false); @@ -349,6 +351,18 @@ export const ToasterContainer = ({ {t("magneto.board.notify")} )} + {selectedBoardsIds.length == 1 && + selectedBoardRights != null && + selectedBoardRights.contrib && ( + + )} {!isPublic && allBoardsMine() && areFoldersMine() && (