Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71486efeef | |||
| 8213f6213f | |||
| f8c9fec77e | |||
| af4be850cb | |||
| ca1a13ed9d | |||
| 1873da6c49 |
@@ -61,6 +61,7 @@ endif(COLOR_MANAGMENT)
|
|||||||
|
|
||||||
qt_add_resources(TENMON_SRC resources/resources.qrc)
|
qt_add_resources(TENMON_SRC resources/resources.qrc)
|
||||||
qt_add_resources(TENMON_SRC shaders/shaders.qrc)
|
qt_add_resources(TENMON_SRC shaders/shaders.qrc)
|
||||||
|
qt_add_resources(TENMON_SRC scripts/scripts.qrc)
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
list(APPEND TENMON_SRC resources/icon.rc)
|
list(APPEND TENMON_SRC resources/icon.rc)
|
||||||
set(tenmon_ICON "")
|
set(tenmon_ICON "")
|
||||||
|
|||||||
+10
-1
@@ -57,7 +57,10 @@ void BatchProcessing::scanScriptDir()
|
|||||||
|
|
||||||
_ui->scriptsList->clear();
|
_ui->scriptsList->clear();
|
||||||
QDir dir(_scriptBasePath);
|
QDir dir(_scriptBasePath);
|
||||||
|
QDir embededDir(":/scripts");
|
||||||
QStringList scripts = dir.entryList(QDir::Files | QDir::Readable);
|
QStringList scripts = dir.entryList(QDir::Files | QDir::Readable);
|
||||||
|
scripts.append(embededDir.entryList(QDir::Files));
|
||||||
|
scripts.removeDuplicates();
|
||||||
_ui->scriptsList->addItems(scripts);
|
_ui->scriptsList->addItems(scripts);
|
||||||
|
|
||||||
int idx = scripts.indexOf(current);
|
int idx = scripts.indexOf(current);
|
||||||
@@ -207,7 +210,13 @@ void BatchProcessing::runScript()
|
|||||||
QFileInfo outDir(_ui->outputPath->text());
|
QFileInfo outDir(_ui->outputPath->text());
|
||||||
if(outDir.exists() && outDir.isWritable())
|
if(outDir.exists() && outDir.isWritable())
|
||||||
{
|
{
|
||||||
_engineThread->setParams(_scriptBasePath + selectedItems.first()->text(), scanDirectories(paths), _ui->outputPath->text());
|
QString script = selectedItems.first()->text();
|
||||||
|
if(QDir(_scriptBasePath).exists(script))
|
||||||
|
script = _scriptBasePath + script;
|
||||||
|
else
|
||||||
|
script = ":/scripts/" + script;
|
||||||
|
|
||||||
|
_engineThread->setParams(script, scanDirectories(paths), _ui->outputPath->text());
|
||||||
_engineThread->start();
|
_engineThread->start();
|
||||||
_ui->startButton->setEnabled(false);
|
_ui->startButton->setEnabled(false);
|
||||||
_ui->stopButton->setEnabled(true);
|
_ui->stopButton->setEnabled(true);
|
||||||
|
|||||||
@@ -188,12 +188,6 @@ void ImageWidget::bestFit()
|
|||||||
setOffset(0, 0);
|
setOffset(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImageWidget::blockRepaint(bool block)
|
|
||||||
{
|
|
||||||
m_blockRepaint = block;
|
|
||||||
if(!block)update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageWidget::allocateThumbnails(const QStringList &paths)
|
void ImageWidget::allocateThumbnails(const QStringList &paths)
|
||||||
{
|
{
|
||||||
makeCurrent();
|
makeCurrent();
|
||||||
@@ -323,8 +317,6 @@ void ImageWidget::showThumbnail(bool enable)
|
|||||||
|
|
||||||
void ImageWidget::paintGL()
|
void ImageWidget::paintGL()
|
||||||
{
|
{
|
||||||
if(m_blockRepaint)return;
|
|
||||||
|
|
||||||
float dx = m_dx;
|
float dx = m_dx;
|
||||||
float dy = m_dy;
|
float dy = m_dy;
|
||||||
if(m_width > m_image->width() * m_scale)
|
if(m_width > m_image->width() * m_scale)
|
||||||
@@ -333,6 +325,7 @@ void ImageWidget::paintGL()
|
|||||||
dy = -height() * 0.5f + m_image->height() * m_scale * 0.5f;
|
dy = -height() * 0.5f + m_image->height() * m_scale * 0.5f;
|
||||||
QBrush highlight = style()->standardPalette().highlight();
|
QBrush highlight = style()->standardPalette().highlight();
|
||||||
|
|
||||||
|
f->glClear(GL_COLOR_BUFFER_BIT);
|
||||||
if(m_showThumbnails)
|
if(m_showThumbnails)
|
||||||
{
|
{
|
||||||
m_vaoThumb->bind();
|
m_vaoThumb->bind();
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ class ImageWidget : public QOpenGLWidget
|
|||||||
float m_scale = 1.0f;
|
float m_scale = 1.0f;
|
||||||
int m_scaleStop = 0;
|
int m_scaleStop = 0;
|
||||||
bool m_bestFit = false;
|
bool m_bestFit = false;
|
||||||
bool m_blockRepaint = false;
|
|
||||||
bool m_bwImg = false;
|
bool m_bwImg = false;
|
||||||
bool m_falseColor = false;
|
bool m_falseColor = false;
|
||||||
bool m_invert = false;
|
bool m_invert = false;
|
||||||
@@ -78,7 +77,6 @@ public:
|
|||||||
void setWCS(std::shared_ptr<WCSData> wcs);
|
void setWCS(std::shared_ptr<WCSData> wcs);
|
||||||
void zoom(int zoom, const QPointF &mousePos = QPointF());
|
void zoom(int zoom, const QPointF &mousePos = QPointF());
|
||||||
void bestFit();
|
void bestFit();
|
||||||
void blockRepaint(bool block);
|
|
||||||
void allocateThumbnails(const QStringList &paths);
|
void allocateThumbnails(const QStringList &paths);
|
||||||
QVector2D getImagePixelCoord(const QVector2D &pos);
|
QVector2D getImagePixelCoord(const QVector2D &pos);
|
||||||
void setBayerMask(int mask);
|
void setBayerMask(int mask);
|
||||||
|
|||||||
+1
-1
Submodule libXISF updated: 922d4b73c9...d00de2041d
+20
-20
@@ -143,22 +143,22 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
|||||||
connect(m_imageGL->imageWidget(), &ImageWidget::fileDropped, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
|
connect(m_imageGL->imageWidget(), &ImageWidget::fileDropped, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
|
||||||
|
|
||||||
QMenu *fileMenu = new QMenu(tr("File"), this);
|
QMenu *fileMenu = new QMenu(tr("File"), this);
|
||||||
fileMenu->addAction(tr("Open"), this, SLOT(loadFile()), QKeySequence::Open);
|
fileMenu->addAction(tr("Open"), QKeySequence::Open, this, SLOT(loadFile()));
|
||||||
fileMenu->addAction(tr("Open directory recursively"), this, &MainWindow::loadDir);
|
fileMenu->addAction(tr("Open directory recursively"), this, &MainWindow::loadDir);
|
||||||
fileMenu->addAction(tr("Save as"), this, SLOT(saveAs()), QKeySequence::Save);
|
fileMenu->addAction(tr("Save as"), QKeySequence::Save, this, SLOT(saveAs()));
|
||||||
fileMenu->addSeparator();
|
fileMenu->addSeparator();
|
||||||
fileMenu->addAction(tr("Copy marked files"), this, SLOT(copyMarked()), Qt::Key_F5);
|
fileMenu->addAction(tr("Copy marked files"), Qt::Key_F5, this, SLOT(copyMarked()));
|
||||||
fileMenu->addAction(tr("Move marked files"), this, SLOT(moveMarked()), Qt::Key_F6);
|
fileMenu->addAction(tr("Move marked files"), Qt::Key_F6, this, SLOT(moveMarked()));
|
||||||
fileMenu->addAction(tr("Move marked files to trash"), this, &MainWindow::deleteMarked, QKeySequence::Delete);
|
fileMenu->addAction(tr("Move marked files to trash"), QKeySequence::Delete, this, &MainWindow::deleteMarked);
|
||||||
fileMenu->addSeparator();
|
fileMenu->addSeparator();
|
||||||
fileMenu->addAction(tr("Index directory"), this, SLOT(indexDir()));
|
fileMenu->addAction(tr("Index directory"), this, SLOT(indexDir()));
|
||||||
fileMenu->addAction(tr("Reindex files"), this, SLOT(reindex()));
|
fileMenu->addAction(tr("Reindex files"), this, SLOT(reindex()));
|
||||||
fileMenu->addAction(tr("Export database to CSV"), this, &MainWindow::exportCSV);
|
fileMenu->addAction(tr("Export database to CSV"), this, &MainWindow::exportCSV);
|
||||||
fileMenu->addAction(tr("Batch processing"), [this](){
|
fileMenu->addAction(tr("Batch processing"), Qt::Key_B | Qt::CTRL, [this](){
|
||||||
BatchProcessing *batchProcessing = new BatchProcessing(this);
|
BatchProcessing *batchProcessing = new BatchProcessing(this);
|
||||||
batchProcessing->exec();
|
batchProcessing->exec();
|
||||||
delete batchProcessing;
|
delete batchProcessing;
|
||||||
}, Qt::Key_B | Qt::CTRL);
|
});
|
||||||
fileMenu->addSeparator();
|
fileMenu->addSeparator();
|
||||||
QAction *liveModeAction = fileMenu->addAction(tr("Live mode"), this, SLOT(liveMode(bool)));
|
QAction *liveModeAction = fileMenu->addAction(tr("Live mode"), this, SLOT(liveMode(bool)));
|
||||||
liveModeAction->setCheckable(true);
|
liveModeAction->setCheckable(true);
|
||||||
@@ -171,9 +171,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
|||||||
menuBar()->addMenu(editMenu);
|
menuBar()->addMenu(editMenu);
|
||||||
|
|
||||||
QMenu *viewMenu = new QMenu(tr("View"), this);
|
QMenu *viewMenu = new QMenu(tr("View"), this);
|
||||||
viewMenu->addAction(tr("Zoom In"), m_imageGL, SLOT(zoomIn()), QKeySequence::ZoomIn);
|
viewMenu->addAction(tr("Zoom In"), QKeySequence::ZoomIn, m_imageGL, SLOT(zoomIn()));
|
||||||
viewMenu->addAction(tr("Zoom Out"), m_imageGL, SLOT(zoomOut()), QKeySequence::ZoomOut);
|
viewMenu->addAction(tr("Zoom Out"), QKeySequence::ZoomOut, m_imageGL, SLOT(zoomOut()));
|
||||||
viewMenu->addAction(tr("Best Fit"), m_imageGL, SLOT(bestFit()), QKeySequence("Ctrl+1"));
|
viewMenu->addAction(tr("Best Fit"), QKeySequence("Ctrl+1"), m_imageGL, SLOT(bestFit()));
|
||||||
viewMenu->addAction(tr("100%"), m_imageGL, SLOT(oneToOne()));
|
viewMenu->addAction(tr("100%"), m_imageGL, SLOT(oneToOne()));
|
||||||
viewMenu->addSeparator();
|
viewMenu->addSeparator();
|
||||||
QMenu *bayerMenu = viewMenu->addMenu(tr("Bayer mask"));
|
QMenu *bayerMenu = viewMenu->addMenu(tr("Bayer mask"));
|
||||||
@@ -195,28 +195,28 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
|||||||
settings.setValue("mainwindow/bayermask", data);
|
settings.setValue("mainwindow/bayermask", data);
|
||||||
});
|
});
|
||||||
|
|
||||||
QAction *thumbnailsAction = viewMenu->addAction(tr("Thumbnails"), [this](bool checked){
|
QAction *thumbnailsAction = viewMenu->addAction(tr("Thumbnails"), Qt::Key_F2, [this](bool checked){
|
||||||
if(SettingsDialog::loadThumbsizes())m_ringList->clearThumbnails();
|
if(SettingsDialog::loadThumbsizes())m_ringList->clearThumbnails();
|
||||||
m_imageGL->imageWidget()->allocateThumbnails(m_ringList->imageNames());
|
m_imageGL->imageWidget()->allocateThumbnails(m_ringList->imageNames());
|
||||||
m_imageGL->imageWidget()->showThumbnail(checked);
|
m_imageGL->imageWidget()->showThumbnail(checked);
|
||||||
if(checked)m_ringList->loadThumbnails();
|
if(checked)m_ringList->loadThumbnails();
|
||||||
else m_ringList->stopLoading();
|
else m_ringList->stopLoading();
|
||||||
}, Qt::Key_F2);
|
});
|
||||||
thumbnailsAction->setCheckable(true);
|
thumbnailsAction->setCheckable(true);
|
||||||
QAction *slideshowAction = viewMenu->addAction(tr("Slideshow"), m_ringList, &ImageRingList::toggleSlideshow, Qt::Key_F3);
|
QAction *slideshowAction = viewMenu->addAction(tr("Slideshow"), Qt::Key_F3, m_ringList, &ImageRingList::toggleSlideshow);
|
||||||
slideshowAction->setCheckable(true);
|
slideshowAction->setCheckable(true);
|
||||||
menuBar()->addMenu(viewMenu);
|
menuBar()->addMenu(viewMenu);
|
||||||
|
|
||||||
QMenu *selectMenu = new QMenu(tr("Select"), this);
|
QMenu *selectMenu = new QMenu(tr("Select"), this);
|
||||||
selectMenu->addAction(tr("Mark"), this, SLOT(markImage()), Qt::Key_F7);
|
selectMenu->addAction(tr("Mark"), Qt::Key_F7, this, SLOT(markImage()));
|
||||||
selectMenu->addAction(tr("Unmark"), this, SLOT(unmarkImage()), Qt::Key_F8);
|
selectMenu->addAction(tr("Unmark"), Qt::Key_F8, this, SLOT(unmarkImage()));
|
||||||
selectMenu->addSeparator();
|
selectMenu->addSeparator();
|
||||||
selectMenu->addAction(tr("Mark and next"), this, SLOT(markAndNext()), Qt::Key_M);
|
selectMenu->addAction(tr("Mark and next"), Qt::Key_M, this, SLOT(markAndNext()));
|
||||||
selectMenu->addAction(tr("Unmark and next"), this, SLOT(unmarkAndNext()), Qt::Key_X);
|
selectMenu->addAction(tr("Unmark and next"), Qt::Key_X, this, SLOT(unmarkAndNext()));
|
||||||
selectMenu->addAction(tr("Show marked"), this, &MainWindow::showMarkFilesDialog);
|
selectMenu->addAction(tr("Show marked"), this, &MainWindow::showMarkFilesDialog);
|
||||||
menuBar()->addMenu(selectMenu);
|
menuBar()->addMenu(selectMenu);
|
||||||
|
|
||||||
QMenu *analyzeMenu = new QMenu(tr("Analyze"), this);
|
/*QMenu *analyzeMenu = new QMenu(tr("Analyze"), this);
|
||||||
QActionGroup *analyzeGroup = new QActionGroup(this);
|
QActionGroup *analyzeGroup = new QActionGroup(this);
|
||||||
connect(analyzeGroup, &QActionGroup::triggered, [](QAction* action) {
|
connect(analyzeGroup, &QActionGroup::triggered, [](QAction* action) {
|
||||||
static QAction* lastAction = nullptr;
|
static QAction* lastAction = nullptr;
|
||||||
@@ -239,7 +239,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
|||||||
connect(peakAction, SIGNAL(toggled(bool)), this, SLOT(peakFinder(bool)));
|
connect(peakAction, SIGNAL(toggled(bool)), this, SLOT(peakFinder(bool)));
|
||||||
connect(starAction, SIGNAL(toggled(bool)), this, SLOT(starFinder(bool)));
|
connect(starAction, SIGNAL(toggled(bool)), this, SLOT(starFinder(bool)));
|
||||||
analyzeMenu->addActions({statsAction, peakAction, starAction});
|
analyzeMenu->addActions({statsAction, peakAction, starAction});
|
||||||
//menuBar()->addMenu(analyzeMenu);
|
menuBar()->addMenu(analyzeMenu);*/
|
||||||
|
|
||||||
QMenu *dockMenu = new QMenu(tr("Docks"), this);
|
QMenu *dockMenu = new QMenu(tr("Docks"), this);
|
||||||
dockMenu->addAction(infoDock->toggleViewAction());
|
dockMenu->addAction(infoDock->toggleViewAction());
|
||||||
@@ -251,7 +251,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
|||||||
menuBar()->addMenu(dockMenu);
|
menuBar()->addMenu(dockMenu);
|
||||||
|
|
||||||
QMenu *helpMenu = menuBar()->addMenu(tr("Help"));
|
QMenu *helpMenu = menuBar()->addMenu(tr("Help"));
|
||||||
helpMenu->addAction(tr("Help"), [this]{ HelpDialog help(this); help.exec(); }, QKeySequence::HelpContents);
|
helpMenu->addAction(tr("Help"), QKeySequence::HelpContents, [this]{ HelpDialog help(this); help.exec(); });
|
||||||
helpMenu->addAction(tr("About Tenmon"), [this]{ About about(this); about.exec(); });
|
helpMenu->addAction(tr("About Tenmon"), [this]{ About about(this); about.exec(); });
|
||||||
helpMenu->addAction(tr("About Qt"), [this](){ QMessageBox::aboutQt(this); });
|
helpMenu->addAction(tr("About Qt"), [this](){ QMessageBox::aboutQt(this); });
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
core.log("This script convert any FITS file into XISF with ZSTD compression");
|
||||||
|
|
||||||
|
if(files.length == 0)
|
||||||
|
{
|
||||||
|
core.log("No input files");
|
||||||
|
throw "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let compression = {"compressionType": "zstd+sh"};
|
||||||
|
|
||||||
|
for(file of files)
|
||||||
|
{
|
||||||
|
if(file.suffix() == "fits" || file.suffix() == "fit")
|
||||||
|
{
|
||||||
|
core.log("Converting " + file.fileName());
|
||||||
|
file.convertAsync(file.relativeFilePath(), "XISF", compression);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// how to get input from user
|
||||||
|
let d = core.getFloat("Getting float value");
|
||||||
|
let i = core.getInt("Getting integer value");
|
||||||
|
let s = core.getString("Getting string value");
|
||||||
|
|
||||||
|
// print user input
|
||||||
|
core.log("Your input " + d + " " + i + " " + s);
|
||||||
|
|
||||||
|
for(file of files)
|
||||||
|
{
|
||||||
|
if(file.suffix() == "fits" || file.suffix() == "fit" || file.suffix() == "xisf")
|
||||||
|
{
|
||||||
|
let keywords = file.fitsKeywords();
|
||||||
|
let item = core.getItem(keywords, "Select keyword");
|
||||||
|
core.log("You selected keyword " + item); core.log(file.fitsKeywords());
|
||||||
|
|
||||||
|
core.log("fileName() " + file.fileName());
|
||||||
|
core.log("absoluteFilePath() " + file.absoluteFilePath());
|
||||||
|
core.log("absolutePath() " + file.absolutePath());
|
||||||
|
core.log("relativeFilePath() " + file.relativeFilePath());
|
||||||
|
core.log("relativePath() " + file.relativePath());
|
||||||
|
core.log("baseName() " + file.baseName());
|
||||||
|
core.log("completeBase() " + file.completeBaseName());
|
||||||
|
core.log("suffix() " + file.suffix());
|
||||||
|
core.log("size() " + file.size());
|
||||||
|
let stats = file.stats();
|
||||||
|
core.log("Image statistics");
|
||||||
|
core.log("\tMinimum: " + stats.min);
|
||||||
|
core.log("\tMaximum: " + stats.max);
|
||||||
|
core.log("\tMedian:" + stats.median);
|
||||||
|
core.log("\tStandard deviation:" + stats.stddev);
|
||||||
|
core.log("\tMedian Absolute Deviation:" + stats.mad);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
for(file of files)
|
||||||
|
{
|
||||||
|
if(file.suffix() == "fits" || file.suffix() == "fit" || file.suffix() == "xisf")
|
||||||
|
{
|
||||||
|
let stats = file.stats();
|
||||||
|
core.log("File: \"" + file.fileName() + "\" - median: " + stats.median);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
function checkFITS(key)
|
||||||
|
{
|
||||||
|
const noEditableKey = ["SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "EXTEND", "BZERO", "BSCALE"];
|
||||||
|
return noEditableKey.indexOf(key) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(files.length == 0)
|
||||||
|
{
|
||||||
|
core.log("No input files");
|
||||||
|
throw "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let action = core.getItem(["UPDATE", "ADD", "REMOVE"], "Do you want update, add or remove record?");
|
||||||
|
|
||||||
|
let modify = new FITSRecordModify();
|
||||||
|
|
||||||
|
if(action == "UPDATE")
|
||||||
|
{
|
||||||
|
let keywords = files[0].fitsKeywords().filter(checkFITS);
|
||||||
|
let keyword = core.getItem(keywords, "Select keyword to update");
|
||||||
|
let value = files[0].fitsValue(keyword);
|
||||||
|
if(isNaN(value))
|
||||||
|
value = core.getString("Enter new value", value);
|
||||||
|
else
|
||||||
|
value = core.getFloat("Enter new value", value);
|
||||||
|
modify.updateKeyword(keyword, value);
|
||||||
|
}
|
||||||
|
else if(action == "ADD")
|
||||||
|
{
|
||||||
|
let keyword = core.getString("Enter keyword to add");
|
||||||
|
let value = core.getString("Enter new value");
|
||||||
|
keyword = keyword.toUpperCase();
|
||||||
|
modify.addKeyword(keyword, value);
|
||||||
|
}
|
||||||
|
else if(action == "REMOVE")
|
||||||
|
{
|
||||||
|
let keywords = files[0].fitsKeywords().filter(checkFITS);
|
||||||
|
let keyword = core.getItem(keywords, "Select keyword to remove");
|
||||||
|
modify.removeKeyword(keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(file of files)
|
||||||
|
{
|
||||||
|
if(file.suffix() == "fits" || file.suffix() == "fit" || file.suffix() == "xisf")
|
||||||
|
{
|
||||||
|
core.log("Modifing " + file.fileName());
|
||||||
|
file.modifyFITSRecords(modify);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<RCC>
|
||||||
|
<qresource prefix="/scripts">
|
||||||
|
<file>example script</file>
|
||||||
|
<file>convert to XISF</file>
|
||||||
|
<file>median</file>
|
||||||
|
<file>modify FITS header</file>
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
<content_rating type="oars-1.1"/>
|
<content_rating type="oars-1.1"/>
|
||||||
<releases>
|
<releases>
|
||||||
<release version="20240610" date="2024-06-10">
|
<release version="20240616" date="2024-06-16">
|
||||||
<description>
|
<description>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Batch processing with JavaScript</li>
|
<li>Batch processing with JavaScript</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user