diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java index f0d294d..f264aa9 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java @@ -25,10 +25,10 @@ import jakarta.annotation.security.PermitAll; @PageTitle("Artist | Comics | Kontor") public class ArtistView extends VerticalLayout { - Grid grid = new Grid<>(Artist.class); - TextField filterText = new TextField(); - ArtistForm form; - ComicService service; + Grid grid = new Grid<>(Artist.class); + TextField filterText = new TextField(); + ArtistForm form; + ComicService service; public ArtistView(ComicService service) { this.service = service; diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java b/springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java index 866f7b3..d51ba8a 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java @@ -2,6 +2,7 @@ package de.thpeetz.kontor.media; import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.sidenav.SideNavItem; +import de.thpeetz.kontor.media.views.MediaActorView; import de.thpeetz.kontor.media.views.MediaArticleView; import de.thpeetz.kontor.media.views.MediaFileView; import de.thpeetz.kontor.media.views.MediaVideoView; @@ -15,9 +16,11 @@ public class MediaConstants { public static final String MEDIAVIDEO_ROUTE = "media/mediavideo"; public static final String MEDIAARTICLE_ROUTE = "media/mediaarticle"; public static final String MEDIA_ROLE = "ROLE_MEDIA"; + public static final String MEDIAACTOR_ROUTE = "media/mediaactor"; private static final String MEDIAFILE = "Media Files"; private static final String MEDIAVIDEO = "Media Videos"; private static final String MEDIAARTICLE = "Media Article"; + private static final String MEDIAACTOR = "Media Actor"; public static SideNavItem getMediaNavigation(ArrayList roles) { SideNavItem media = new SideNavItem(MEDIA, MEDIAFILE_ROUTE, VaadinIcon.VIMEO.create()); @@ -25,6 +28,7 @@ public class MediaConstants { media.addItem(new SideNavItem(MEDIAARTICLE, MediaArticleView.class)); if (roles.contains(MEDIA_ROLE)) { media.addItem(new SideNavItem(MEDIAFILE, MediaFileView.class)); + media.addItem(new SideNavItem(MEDIAACTOR, MediaActorView.class)); } return media; } diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java new file mode 100644 index 0000000..25cc196 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java @@ -0,0 +1,28 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Slf4j +@Entity +public class MediaActor extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "media_actor", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List mediaActorFiles; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorFile.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorFile.java new file mode 100644 index 0000000..7d8c511 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorFile.java @@ -0,0 +1,30 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +@Table(indexes = {@Index(columnList = "media_file_id, media_actor_id") }, + uniqueConstraints = @UniqueConstraint(columnNames = {"media_file_id", "media_actor_id" }) +) +public class MediaActorFile extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "media_file_id") + @NotNull + private MediaFile media_file; + + @ManyToOne + @JoinColumn(name = "media_actor_id") + @NotNull + private MediaActor media_actor; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorFileRepository.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorFileRepository.java new file mode 100644 index 0000000..5f364b7 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorFileRepository.java @@ -0,0 +1,6 @@ +package de.thpeetz.kontor.media.data; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MediaActorFileRepository extends JpaRepository { +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorRepository.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorRepository.java new file mode 100644 index 0000000..973e00a --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.media.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface MediaActorRepository extends JpaRepository { + @Query("select m from MediaActor m " + + "where lower(m.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findByNameIgnoreCase(String name); + + MediaActor findByName(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java index fa7b934..39be1c2 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java @@ -2,14 +2,14 @@ package de.thpeetz.kontor.media.data; import de.thpeetz.kontor.common.data.AbstractEntity; import jakarta.annotation.Nullable; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; +import jakarta.persistence.*; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import java.util.List; + @Slf4j @Getter @Setter @@ -37,4 +37,7 @@ public class MediaFile extends AbstractEntity { @Nullable private String path; + @OneToMany(fetch = FetchType.EAGER, mappedBy = "media_file", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List mediaActorFiles; } diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java index ffb0a24..aa36066 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java @@ -1,7 +1,6 @@ package de.thpeetz.kontor.media.services; -import de.thpeetz.kontor.media.data.MediaFile; -import de.thpeetz.kontor.media.data.MediaFileRepository; +import de.thpeetz.kontor.media.data.*; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -13,19 +12,22 @@ public class MediaFileService { private final MediaFileRepository mediaFileRepository; - public MediaFileService(MediaFileRepository mediaFileRepository) { + private final MediaActorRepository mediaActorRepository; + + public MediaFileService(MediaFileRepository mediaFileRepository, MediaActorRepository mediaActorRepository) { this.mediaFileRepository = mediaFileRepository; + this.mediaActorRepository = mediaActorRepository; } public List findAllMediaFiles(String stringFilter) { + List results; if (stringFilter == null || stringFilter.isEmpty()) { - log.debug("Found " + mediaFileRepository.count()+ " entries"); - return mediaFileRepository.findAll(); + results = mediaFileRepository.findAll(); } else { - List results = mediaFileRepository.search(stringFilter); - log.debug("Found " + results.size() + " entries"); - return results; + results = mediaFileRepository.search(stringFilter); } + log.debug("Found " + results.size() + " entries"); + return results; } public void saveMediaFile(MediaFile mediaFile) { @@ -39,4 +41,27 @@ public class MediaFileService { public void deleteMediaFile(MediaFile mediaFile) { mediaFileRepository.delete(mediaFile); } + + public List findAllMediaActors(String stringFilter) { + List results; + if (stringFilter == null || stringFilter.isEmpty()) { + results = mediaActorRepository.findAll(); + } else { + results = mediaActorRepository.search(stringFilter); + } + log.debug("Found " + results.size() + " entries"); + return results; + } + + public void saveMediaActor(MediaActor mediaActor) { + if (mediaActor == null) { + log.warn("MediaActor is null. Are you sure you have connected your form to the application?"); + return; + } + mediaActorRepository.save(mediaActor); + } + + public void deleteMediaActor(MediaActor mediaActor) { + mediaActorRepository.delete(mediaActor); + } } diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java new file mode 100644 index 0000000..e7cab60 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java @@ -0,0 +1,116 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaActor; +import de.thpeetz.kontor.media.data.MediaActorFile; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class MediaActorForm extends FormLayout { + + TextField name = new TextField("Name"); + // Grid mediaActorFiles = new Grid<>(MediaActorFile.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MediaActor.class); + + public MediaActorForm() { + addClassName("media-actor-form"); + binder.bindInstanceFields(this); + + // mediaActorFiles.setColumns("media_file.title"); + // mediaActorFiles.getColumnByKey("mediaFile.title").setHeader("MediaFile"); + // mediaActorFiles.getColumns().forEach(col -> col.setAutoWidth(true)); + // add(name, mediaActorFiles, createButtonsLayout()); + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new MediaActorForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new MediaActorForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new MediaActorForm.SaveEvent(this, binder.getBean())); + } + } + + public void setMediaActor(MediaActor mediaActor) { + binder.setBean(mediaActor); + } + + public void setMediaActorFiles(List mediaActorFiles) { + log.info("Setting comic works: {}", mediaActorFiles); + // this.mediaActorFiles.setItems(mediaActorFiles); + } + + public abstract static class MediaActorFormEvent extends ComponentEvent { + private MediaActor mediaActor; + + protected MediaActorFormEvent(MediaActorForm source, MediaActor mediaActor) { + super(source, false); + this.mediaActor = mediaActor; + } + + public MediaActor getMediaActor() { + return mediaActor; + } + } + + public static class SaveEvent extends MediaActorForm.MediaActorFormEvent { + SaveEvent(MediaActorForm source, MediaActor mediaActor) { + super(source, mediaActor); + } + } + + public static class DeleteEvent extends MediaActorForm.MediaActorFormEvent { + DeleteEvent(MediaActorForm source, MediaActor mediaActor) { + super(source, mediaActor); + } + } + + public static class CloseEvent extends MediaActorForm.MediaActorFormEvent { + CloseEvent(MediaActorForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(MediaActorForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(MediaActorForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(MediaActorForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java new file mode 100644 index 0000000..40727cb --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java @@ -0,0 +1,125 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.views.ArtistForm; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.media.data.MediaActor; +import de.thpeetz.kontor.media.services.MediaFileService; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = MediaConstants.MEDIAACTOR_ROUTE, layout = MainLayout.class) +@PageTitle("Actor | Media | Kontor") +public class MediaActorView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(MediaActor.class); + TextField filterText = new TextField(); + @Getter + MediaActorForm form; + MediaFileService service; + + public MediaActorView(MediaFileService service) { + this.service = service; + addClassName("media-actor-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("artist-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editMediaActor(event.getValue())); + } + + private void configureForm() { + form = new MediaActorForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveMediaActor); + form.addDeleteListener(this::deleteMediaActor); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMediaActor(MediaActorForm.SaveEvent event) { + service.saveMediaActor(event.getMediaActor()); + updateList(); + closeEditor(); + } + + private void deleteMediaActor(MediaActorForm.DeleteEvent event) { + service.deleteMediaActor(event.getMediaActor()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addMediaActorButton = new Button("Add actor"); + addMediaActorButton.addClickListener(click -> addMediaActor()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addMediaActorButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMediaActor(MediaActor mediaActor) { + if (mediaActor == null) { + closeEditor(); + } else { + form.setMediaActor(mediaActor); + form.setMediaActorFiles(mediaActor.getMediaActorFiles()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setMediaActor(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMediaActor() { + grid.asSingleSelect().clear(); + editMediaActor(new MediaActor()); + } + + public void updateList() { + grid.setItems(service.findAllMediaActors(filterText.getValue())); + } +}