Commit cd46b0af authored by PECQUOT's avatar PECQUOT

[enh] Refactor photos UI (Mantis #48755)

Signed-off-by: PECQUOT's avatarlp1ee9d <ludovic.pecquot@e-is.pro>
parent b6f1b554
......@@ -23,10 +23,18 @@ package fr.ifremer.reefdb.ui.swing.content.observation.photo;
* #L%
*/
import com.google.common.collect.ImmutableList;
import fr.ifremer.quadrige3.core.service.http.HttpNotFoundException;
import fr.ifremer.reefdb.service.ReefDbBusinessException;
import fr.ifremer.reefdb.service.ReefDbServiceLocator;
import fr.ifremer.reefdb.ui.swing.action.AbstractReefDbAction;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.nuiton.i18n.I18n.t;
......@@ -35,7 +43,9 @@ import static org.nuiton.i18n.I18n.t;
*/
public class DownloadAction extends AbstractReefDbAction<PhotosTabUIModel, PhotosTabUI, PhotosTabUIHandler> {
private boolean downloaded;
private static final Log LOG = LogFactory.getLog(DownloadAction.class);
private boolean allDownloaded;
private Collection<PhotosTableRowModel> toDownload;
/**
* Constructor.
......@@ -44,6 +54,19 @@ public class DownloadAction extends AbstractReefDbAction<PhotosTabUIModel, Photo
*/
public DownloadAction(PhotosTabUIHandler handler) {
super(handler, false);
setActionDescription(t("reefdb.photo.download.tip"));
}
public Collection<PhotosTableRowModel> getToDownload() {
return Optional.ofNullable(toDownload).orElse(getModel().getSelectedRows());
}
public void setToDownload(PhotosTableRowModel toDownload) {
this.toDownload = ImmutableList.of(toDownload);
}
public void setToDownload(Collection<PhotosTableRowModel> toDownload) {
this.toDownload = toDownload;
}
/**
......@@ -51,13 +74,7 @@ public class DownloadAction extends AbstractReefDbAction<PhotosTabUIModel, Photo
*/
@Override
public boolean prepareAction() throws Exception {
if (!super.prepareAction() || getModel().getSelectedRows().isEmpty()) {
return false;
}
createProgressionUIModel();
downloaded = false;
return getModel().getSingleSelectedRow().isFileDownloadable();
return super.prepareAction() && !getToDownload().isEmpty() && getToDownload().stream().anyMatch(PhotosTableRowModel::isFileDownloadable);
}
/**
......@@ -66,17 +83,32 @@ public class DownloadAction extends AbstractReefDbAction<PhotosTabUIModel, Photo
@Override
public void doAction() throws Exception {
try {
downloaded = ReefDbServiceLocator.instance().getSynchroRestClientService().downloadPhoto(
getContext().getAuthenticationInfo(),
getModel().getSingleSelectedRow().getRemoteId(),
getModel().getSingleSelectedRow().getFullPath(),
getProgressionUIModel()
);
} catch (HttpNotFoundException e) {
throw new ReefDbBusinessException(t("reefdb.photo.download.notFound", getModel().getSingleSelectedRow().getName()));
}
allDownloaded = true;
createProgressionUIModel();
List<PhotosTableRowModel> downloadablePhotos = getToDownload().stream().filter(PhotosTableRowModel::isFileDownloadable).collect(Collectors.toList());
int nbPhotos = downloadablePhotos.size();
int nPhoto = 0;
for (PhotosTableRowModel photo: downloadablePhotos) {
try {
getProgressionUIModel().setMessage(String.format("%s / %s", ++nPhoto, nbPhotos));
boolean downloaded = ReefDbServiceLocator.instance().getSynchroRestClientService().downloadPhoto(
getContext().getAuthenticationInfo(),
photo.getRemoteId(),
photo.getFullPath(),
getProgressionUIModel()
);
if (!downloaded) {
LOG.warn(String.format("the photo %s was not download, but without exception", photo.getName()));
}
allDownloaded &= downloaded;
} catch (HttpNotFoundException e) {
throw new ReefDbBusinessException(t("reefdb.photo.download.notFound", photo.getName()));
}
}
}
/**
......@@ -85,20 +117,15 @@ public class DownloadAction extends AbstractReefDbAction<PhotosTabUIModel, Photo
@Override
public void postSuccessAction() {
if (!downloaded)
if (!allDownloaded)
getContext().getDialogHelper().showErrorDialog(t("reefdb.photo.download.error"));
getHandler().updatePhotoViewerContent();
getHandler().updatePhotoViewerContent(false);
getUI().invalidate();
getUI().repaint();
getUI().processDataBinding(PhotosTabUI.BINDING_DOWNLOAD_PHOTO_BUTTON_ENABLED);
getHandler().updateControls();
super.postSuccessAction();
}
@Override
protected void releaseAction() {
downloaded = false;
super.releaseAction();
}
}
......@@ -24,15 +24,18 @@ package fr.ifremer.reefdb.ui.swing.content.observation.photo;
*/
import fr.ifremer.quadrige3.core.dao.technical.Files;
import fr.ifremer.reefdb.dto.ReefDbBeans;
import fr.ifremer.reefdb.dto.data.photo.PhotoDTO;
import fr.ifremer.quadrige3.ui.swing.ApplicationUIUtil;
import fr.ifremer.reefdb.service.ReefDbBusinessException;
import fr.ifremer.reefdb.ui.swing.action.AbstractReefDbAction;
import javax.swing.JEditorPane;
import javax.swing.JOptionPane;
import javax.swing.event.HyperlinkEvent;
import java.io.File;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import static org.nuiton.i18n.I18n.t;
......@@ -70,10 +73,8 @@ public class ExportAction extends AbstractReefDbAction<PhotosTabUIModel, PhotosT
@Override
public void doAction() throws Exception {
List<String> filePaths = ReefDbBeans.collectProperties(getModel().getSelectedBeans(), PhotoDTO.PROPERTY_FULL_PATH);
for (String filePath: filePaths) {
Path fileSrc = Paths.get(filePath);
for (PhotosTableRowModel photo : getModel().getSelectedRows().stream().filter(PhotosTableRowModel::isFileExists).collect(Collectors.toList())) {
Path fileSrc = Paths.get(photo.getFullPath());
if (!java.nio.file.Files.isRegularFile(fileSrc)) {
throw new ReefDbBusinessException(t("quadrige3.error.file.not.exists"));
}
......@@ -87,7 +88,27 @@ public class ExportAction extends AbstractReefDbAction<PhotosTabUIModel, PhotosT
@Override
public void postSuccessAction() {
displayInfoMessage(t("reefdb.action.photo.export.title"), t("reefdb.action.photo.export.done", destDir.getAbsolutePath()));
String text = t("reefdb.action.photo.export.done",
destDir.getAbsoluteFile().toURI(),
destDir.getAbsolutePath()
);
JEditorPane editorPane = new JEditorPane("text/html", text);
editorPane.setEditable(false);
editorPane.addHyperlinkListener(e -> {
if (HyperlinkEvent.EventType.ACTIVATED == e.getEventType()) {
URL url = e.getURL();
ApplicationUIUtil.openLink(url);
}
});
getContext().getDialogHelper().showOptionDialog(
getUI(),
editorPane,
t("reefdb.action.photo.export.title"),
JOptionPane.INFORMATION_MESSAGE,
JOptionPane.DEFAULT_OPTION
);
super.postSuccessAction();
}
......
......@@ -20,83 +20,78 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
#L%
-->
<Table id="photoTabUI" fill="both" decorator='help' implements='fr.ifremer.reefdb.ui.swing.util.ReefDbUI&lt;PhotosTabUIModel, PhotosTabUIHandler&gt;'>
<import>
fr.ifremer.reefdb.ui.swing.ReefDbHelpBroker
fr.ifremer.reefdb.ui.swing.ReefDbUIContext
fr.ifremer.reefdb.ui.swing.util.ReefDbUI
fr.ifremer.quadrige3.ui.swing.ApplicationUI
fr.ifremer.quadrige3.ui.swing.ApplicationUIUtil
java.awt.FlowLayout
javax.swing.Box
javax.swing.BoxLayout
java.awt.BorderLayout
<JPanel id="photoTabUI" decorator='help' layout="{new BorderLayout()}"
implements='fr.ifremer.reefdb.ui.swing.util.ReefDbUI&lt;PhotosTabUIModel, PhotosTabUIHandler&gt;'>
<import>
fr.ifremer.reefdb.ui.swing.ReefDbHelpBroker
fr.ifremer.reefdb.ui.swing.ReefDbUIContext
fr.ifremer.reefdb.ui.swing.util.ReefDbUI
fr.ifremer.quadrige3.ui.swing.ApplicationUI
fr.ifremer.quadrige3.ui.swing.ApplicationUIUtil
java.awt.FlowLayout
javax.swing.Box
javax.swing.BoxLayout
java.awt.BorderLayout
org.jdesktop.swingx.JXImageView
org.jdesktop.swingx.JXList
org.jdesktop.swingx.JXImageView
org.jdesktop.swingx.JXList
fr.ifremer.reefdb.ui.swing.util.image.PhotoViewer
fr.ifremer.quadrige3.ui.swing.plaf.WaitBlockingLayerUI
fr.ifremer.quadrige3.ui.swing.table.SwingTable
fr.ifremer.quadrige3.ui.swing.component.EnumValueComboBox
fr.ifremer.reefdb.ui.swing.util.image.PhotoViewer
fr.ifremer.quadrige3.ui.swing.plaf.WaitBlockingLayerUI
fr.ifremer.quadrige3.ui.swing.table.SwingTable
fr.ifremer.quadrige3.ui.swing.component.EnumValueComboBox
static org.nuiton.i18n.I18n.*
</import>
static org.nuiton.i18n.I18n.*
</import>
<!--getContextValue est une méthode interne JAXX-->
<PhotosTabUIModel id='model' initializer='getContextValue(PhotosTabUIModel.class)'/>
<!--getContextValue est une méthode interne JAXX-->
<PhotosTabUIModel id='model' initializer='getContextValue(PhotosTabUIModel.class)'/>
<ReefDbHelpBroker id='broker' constructorParams='"reefdb.home.help"'/>
<ReefDbHelpBroker id='broker' constructorParams='"reefdb.home.help"'/>
<BeanValidator id='validator' bean='model' uiClass='jaxx.runtime.validator.swing.ui.ImageValidationUI'>
<field name='instance' component='photoTablePanel'/>
</BeanValidator>
<BeanValidator id='validator' bean='model' uiClass='jaxx.runtime.validator.swing.ui.ImageValidationUI'>
<field name='instance' component='photoTablePanel'/>
</BeanValidator>
<WaitBlockingLayerUI id='photoBlockLayer'/>
<WaitBlockingLayerUI id='photoBlockLayer'/>
<script><![CDATA[
<script><![CDATA[
//Le parent est très utile pour intervenir sur les frères
public PhotosTabUI(ApplicationUI parentUI) {
ApplicationUIUtil.setParentUI(this, parentUI);
}
]]></script>
<row>
<cell weightx='1' weighty='0.7' fill='both'>
<JPanel layout="{new BorderLayout()}">
<JPanel layout="{new BorderLayout()}" constraints="BorderLayout.CENTER">
<PhotoViewer id="photoViewer" genericType="PhotosTableRowModel" decorator="boxed" constraints="BorderLayout.CENTER"/>
<PhotoViewer id="photoViewer" genericType="PhotosTableRowModel" decorator="boxed" constraints="BorderLayout.CENTER"/>
<JPanel constraints="BorderLayout.PAGE_END">
<JButton id="firstPhotoButton" onActionPerformed="handler.firstPhoto()"/>
<JButton id="previousPhotoButton" onActionPerformed="handler.previousPhoto()"/>
<JLabel id="photoIndexLabel"/>
<JButton id="nextPhotoButton" onActionPerformed="handler.nextPhoto()"/>
<JButton id="lastPhotoButton" onActionPerformed="handler.lastPhoto()"/>
<JLabel id="typeDiaporamaLabel"/>
<EnumValueComboBox id="typeDiaporamaComboBox" genericType='PhotoViewType' constructorParams='PhotoViewType.class'/>
<JButton id="fullScreenButton" onActionPerformed="handler.fullScreen()"/>
</JPanel>
</JPanel>
</cell>
</row>
<row>
<cell weightx='1' weighty='0.3' fill='both'>
<JPanel id="photoTablePanel" layout="{new BorderLayout()}">
<JScrollPane constraints="BorderLayout.CENTER">
<SwingTable id='photoTable'/>
</JScrollPane>
<JPanel layout="{new BorderLayout()}" constraints="BorderLayout.PAGE_END">
<JPanel constraints="BorderLayout.LINE_START">
<JButton id='importPhotoButton' alignmentX='{Component.CENTER_ALIGNMENT}'/>
<JButton id='downloadPhotoButton' alignmentX='{Component.CENTER_ALIGNMENT}'/>
<JButton id='deletePhotoButton' alignmentX='{Component.CENTER_ALIGNMENT}' onActionPerformed="handler.removePhoto()"/>
</JPanel>
<JPanel constraints='BorderLayout.LINE_END'>
<JButton id='exportPhotoButton' alignmentX='{Component.CENTER_ALIGNMENT}'/>
</JPanel>
</JPanel>
</JPanel>
</cell>
</row>
</Table>
\ No newline at end of file
<JPanel constraints="BorderLayout.PAGE_END">
<JButton id="firstPhotoButton" onActionPerformed="handler.firstPhoto()"/>
<JButton id="previousPhotoButton" onActionPerformed="handler.previousPhoto()"/>
<JLabel id="photoIndexLabel"/>
<JButton id="nextPhotoButton" onActionPerformed="handler.nextPhoto()"/>
<JButton id="lastPhotoButton" onActionPerformed="handler.lastPhoto()"/>
<JLabel id="typeDiaporamaLabel"/>
<EnumValueComboBox id="typeDiaporamaComboBox" genericType='PhotoViewType' constructorParams='PhotoViewType.class'/>
<JButton id="fullScreenButton" onActionPerformed="handler.fullScreen()"/>
</JPanel>
</JPanel>
<JPanel id="photoTablePanel" layout="{new BorderLayout()}" constraints="BorderLayout.PAGE_END">
<JScrollPane constraints="BorderLayout.CENTER">
<SwingTable id='photoTable'/>
</JScrollPane>
<JPanel layout="{new BorderLayout()}" constraints="BorderLayout.PAGE_END">
<JPanel constraints="BorderLayout.LINE_START">
<JButton id='importPhotoButton' alignmentX='{Component.CENTER_ALIGNMENT}'/>
<JButton id='downloadPhotoButton' alignmentX='{Component.CENTER_ALIGNMENT}'/>
<JButton id='deletePhotoButton' alignmentX='{Component.CENTER_ALIGNMENT}' onActionPerformed="handler.removePhoto()"/>
</JPanel>
<JPanel constraints='BorderLayout.LINE_END'>
<JButton id='exportPhotoButton' alignmentX='{Component.CENTER_ALIGNMENT}'/>
</JPanel>
</JPanel>
</JPanel>
</JPanel>
\ No newline at end of file
......@@ -43,7 +43,7 @@
text: "reefdb.photo.download";
toolTipText: "reefdb.photo.download.tip";
_applicationAction: {DownloadAction.class};
enabled: {model.getSingleSelectedRow().isFileDownloadable()};
enabled: {model.isDownloadEnabled()};
}
#deletePhotoButton {
......@@ -58,7 +58,7 @@
text: "reefdb.common.export";
toolTipText: "reefdb.photo.export.tip";
_applicationAction: {ExportAction.class};
enabled: {!model.getSelectedRows().isEmpty()};
enabled: {model.isExportEnabled()};
}
#firstPhotoButton {
......
......@@ -156,6 +156,10 @@ public class PhotosTabUIModel extends AbstractReefDbTableUIModel<PhotoDTO, Photo
}
public boolean isExportEnabled() {
return !getSelectedRows().isEmpty() && getSelectedRows().stream().allMatch(PhotosTableRowModel::isFileExists);
return !getSelectedRows().isEmpty() && getSelectedRows().stream().anyMatch(PhotosTableRowModel::isFileExists);
}
public boolean isDownloadEnabled() {
return !getSelectedRows().isEmpty() && getSelectedRows().stream().anyMatch(PhotosTableRowModel::isFileDownloadable);
}
}
......@@ -62,7 +62,7 @@ public class PhotoViewer<P extends PhotoDTO> extends JPanel implements ChangeLis
/**
* Constant <code>PROPERTY_DEFAULT_THUMBNAILS_COLUMNS=6</code>
*/
public static final int PROPERTY_DEFAULT_THUMBNAILS_COLUMNS = 6;
private static final int PROPERTY_DEFAULT_THUMBNAILS_COLUMNS = 6;
private final JPanel imageGrid;
private final GridLayout imageGridLayout;
......@@ -83,21 +83,6 @@ public class PhotoViewer<P extends PhotoDTO> extends JPanel implements ChangeLis
imageGrid = new JPanel(imageGridLayout);
imageScroll = new JScrollPane(imageGrid);
add(imageScroll, BorderLayout.CENTER);
imageScroll.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
Component component = findComponentAt(e.getPoint());
if (component instanceof BackgroundPanel) {
for (P photo : photoMap.keySet()) {
if (component.equals(photoMap.get(photo))) {
firePropertyChange(EVENT_PHOTO_CLICKED, null, photo);
return;
}
}
}
}
});
}
public void setType(Images.ImageType type) {
......@@ -112,9 +97,9 @@ public class PhotoViewer<P extends PhotoDTO> extends JPanel implements ChangeLis
/**
* <p>setPhotos.</p>
*
* @param photos a {@link java.util.List} object.
* @param photos a {@link Collection} object.
*/
public void setPhotos(List<P> photos, Images.ImageType type) {
public void setPhotos(Collection<P> photos, Images.ImageType type) {
Assert.notNull(type);
setType(type);
......@@ -158,9 +143,9 @@ public class PhotoViewer<P extends PhotoDTO> extends JPanel implements ChangeLis
/**
* <p>setSelected.</p>
*
* @param photos a {@link java.util.List} object.
* @param photos a {@link Collection} object.
*/
public void setSelected(List<P> photos) {
public void setSelected(Collection<P> photos) {
// reset selected status
photoMap.values().forEach(backgroundPanel -> backgroundPanel.setSelected(false));
......@@ -184,20 +169,66 @@ public class PhotoViewer<P extends PhotoDTO> extends JPanel implements ChangeLis
// draw light gray background
graphics.setColor(new Color(240, 240, 240));
graphics.fillRect(0, 0, type.getMaxSize(), type.getMaxSize());
// draw text
String text = t("reefdb.photo.unavailable");
FontMetrics fm = graphics.getFontMetrics();
float textWidth = fm.stringWidth(text);
float textAscent = fm.getAscent();
// draw texts
graphics.setColor(Color.RED.brighter());
graphics.drawString(text,
(float) type.getMaxSize() / 2 - textWidth / 2,
(float) type.getMaxSize() / 2 + textAscent / 2
);
FontMetrics fm = graphics.getFontMetrics();
List<String> texts = new ArrayList<>();
texts.addAll(adjustText(fm, t("reefdb.photo.unavailable"), type.getMaxSize()));
texts.addAll(adjustText(fm, t("reefdb.photo.unavailable.tip"), type.getMaxSize()));
float textHeight = fm.getHeight();
float textTotalHeight = textHeight * texts.size();
// center in canvas
for (int i = 0; i < texts.size(); i++) {
String text = texts.get(i);
graphics.drawString(text,
(float) type.getMaxSize() / 2 - ((float) fm.stringWidth(text)) / 2,
(float) type.getMaxSize() / 2 - textTotalHeight / 2 + fm.getAscent() + textHeight * i
);
}
}
return image;
}
private List<String> adjustText(FontMetrics fontMetrics, String text, int maxWidth) {
if (text == null)
text = "";
List<String> result = new ArrayList<>();
// separate each word by space
String[] words = text.split("\\s+");
if (words.length == 1) {
// single word
return ImmutableList.of(text);
}
// build line with the first word
StringBuilder line = new StringBuilder(words[0]);
int i = 0;
while (i < words.length - 1) {
// test the length of current line with the next word
if (fontMetrics.stringWidth(line + " " + words[i + 1]) < maxWidth) {
if (line.length() > 0) {
// restore the space on each words except the first one
line.append(" ");
}
// append the word to the current line
line.append(words[++i]);
} else {
// this line is complete, append it to result and create a new one
result.add(line.toString());
line = new StringBuilder();
if (fontMetrics.stringWidth(words[i + 1]) > maxWidth) {
// if the next word is too large, append it to result directly (it will be cropped)
result.add(words[++i]);
}
}
}
if (line.length() > 0) {
// append the remaining line
result.add(line.toString());
}
return result;
}
private void updateImageGrid() {
imageGrid.removeAll();
......@@ -224,17 +255,11 @@ public class PhotoViewer<P extends PhotoDTO> extends JPanel implements ChangeLis
private void initBackgroundPanel(BackgroundPanel image) {
if (type == Images.ImageType.THUMBNAIL) {
image.removeMouseListener(mouseAdapter);
image.removeMouseMotionListener(mouseAdapter);
image.removeMouseWheelListener(mouseAdapter);
image.setCursor(Cursor.getDefaultCursor());
} else {
image.addMouseListener(mouseAdapter);
image.addMouseMotionListener(mouseAdapter);
image.addMouseWheelListener(mouseAdapter);
image.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
}
// Add mouse listener
image.addMouseListener(mouseAdapter);
image.addMouseMotionListener(mouseAdapter);
image.addMouseWheelListener(mouseAdapter);
image.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
if (type == Images.ImageType.BASE) {
createZoomSlider();
......@@ -247,6 +272,20 @@ public class PhotoViewer<P extends PhotoDTO> extends JPanel implements ChangeLis
private Point origin;
@Override
public void mouseClicked(MouseEvent e) {
// fire event on click
if (e.getComponent() instanceof BackgroundPanel) {
for (P photo : photoMap.keySet()) {
if (e.getComponent() == photoMap.get(photo)) {
firePropertyChange(EVENT_PHOTO_CLICKED, null, photo);
return;
}
}
}
}
@Override
public void mousePressed(MouseEvent e) {
origin = new Point(e.getPoint());
......@@ -268,8 +307,6 @@ public class PhotoViewer<P extends PhotoDTO> extends JPanel implements ChangeLis
public void mouseDragged(MouseEvent e) {
if (origin != null) {
JComponent component = (JComponent) e.getSource();
// JViewport viewPort = (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, component);
// if (imageScroll.getViewport() != null) {
int deltaX = origin.x - e.getX();
int deltaY = origin.y - e.getY();
......@@ -278,7 +315,6 @@ public class PhotoViewer<P extends PhotoDTO> extends JPanel implements ChangeLis
view.y += deltaY;
component.scrollRectToVisible(view);
// }
}
}
......
......@@ -926,6 +926,7 @@ reefdb.photo.type.label=
reefdb.photo.type.thumbnail=
reefdb.photo.type.tip=
reefdb.photo.unavailable=
reefdb.photo.unavailable.tip=
reefdb.pluginsUpdater.error=
reefdb.pluginsUpdater.startUpdate=
reefdb.program.editCode.title=
......
......@@ -148,7 +148,7 @@ reefdb.action.filter.export.title=Sélection du répertoire pour l'export des fi
reefdb.action.filter.import.title=Sélection du fichier pour l'import de filtres
reefdb.action.photo.export.chooseDirectory.buttonLabel=Exporter
reefdb.action.photo.export.chooseDirectory.title=Sélection du répertoire pour l'exportation des photos
reefdb.action.photo.export.done=Exportation réussie dans le répertoire\:<br/>%s
reefdb.action.photo.export.done=Exportation réussie dans le répertoire\:<br/><a href\="%s">%s</a>
reefdb.action.photo.export.title=Exportation des photos
reefdb.action.photo.import.chooseFile.buttonLabel=Sélectionnez une photo
reefdb.action.photo.import.chooseFile.filterDescription=Fichier %s uniquement
......@@ -936,7 +936,8 @@ reefdb.photo.type.fullScreen.tip=Affiche la photo originale en plein écran
reefdb.photo.type.label=Affichage
reefdb.photo.type.thumbnail=Miniature
reefdb.photo.type.tip=Le type de la photo
reefdb.photo.unavailable=Photo non disponible
reefdb.photo.unavailable=La photo n'est pas présente sur votre poste.
reefdb.photo.unavailable.tip=Cliquez pour la télécharger.
reefdb.pluginsUpdater.error=Impossible de télécharger la mise à jour du plugin '%s'.<br/>Si le problème persiste, veuillez consulter l'administrateur.
reefdb.pluginsUpdater.startUpdate=Téléchargement et installation d'une nouvelle version du plugin '%s' (version %s)
reefdb.program.editCode.title=Modifier un programme
......
......@@ -50,6 +50,9 @@
<action dev="ludovic.pecquot@e-is.pro" type="add" issue="47995">
Add photo synchronisation
</action>
<action dev="ludovic.pecquot@e-is.pro" type="add" issue="48755">
Refactor photos UI
</action>
</release>
<release version="3.7.2" date="2019-07-25" description="Stable release">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment