Merge branch 'cdrip' of https://github.com/asiviero/Clementine
Fixes #3490 Closes #4113
This commit is contained in:
commit
b594f44c64
@ -1027,9 +1027,13 @@ optional_source(HAVE_AUDIOCD
|
|||||||
SOURCES
|
SOURCES
|
||||||
devices/cddadevice.cpp
|
devices/cddadevice.cpp
|
||||||
devices/cddalister.cpp
|
devices/cddalister.cpp
|
||||||
|
ui/ripcd.cpp
|
||||||
HEADERS
|
HEADERS
|
||||||
devices/cddadevice.h
|
devices/cddadevice.h
|
||||||
devices/cddalister.h
|
devices/cddalister.h
|
||||||
|
ui/ripcd.h
|
||||||
|
UI
|
||||||
|
ui/ripcd.ui
|
||||||
)
|
)
|
||||||
|
|
||||||
# mtp device
|
# mtp device
|
||||||
|
@ -297,7 +297,7 @@ void Transcoder::AddJob(const QString& input,
|
|||||||
// Never overwrite existing files
|
// Never overwrite existing files
|
||||||
if (QFile::exists(job.output)) {
|
if (QFile::exists(job.output)) {
|
||||||
for (int i=0 ; ; ++i) {
|
for (int i=0 ; ; ++i) {
|
||||||
QString new_filename = QString("%1.%2").arg(job.output).arg(i);
|
QString new_filename = QString("%1.%2.%3").arg(job.output.section('.',0,-2)).arg(i).arg(preset.extension_);
|
||||||
if (!QFile::exists(new_filename)) {
|
if (!QFile::exists(new_filename)) {
|
||||||
job.output = new_filename;
|
job.output = new_filename;
|
||||||
break;
|
break;
|
||||||
@ -331,8 +331,10 @@ Transcoder::StartJobStatus Transcoder::MaybeStartNextJob() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Job job = queued_jobs_.takeFirst();
|
Job job = queued_jobs_.takeFirst();
|
||||||
if (StartJob(job))
|
if (StartJob(job)) {
|
||||||
|
emit(JobOutputName(job.output));
|
||||||
return StartedSuccessfully;
|
return StartedSuccessfully;
|
||||||
|
}
|
||||||
|
|
||||||
emit JobComplete(job.input, false);
|
emit JobComplete(job.input, false);
|
||||||
return FailedToStart;
|
return FailedToStart;
|
||||||
|
@ -75,6 +75,7 @@ class Transcoder : public QObject {
|
|||||||
void JobComplete(const QString& filename, bool success);
|
void JobComplete(const QString& filename, bool success);
|
||||||
void LogLine(const QString& message);
|
void LogLine(const QString& message);
|
||||||
void AllJobsComplete();
|
void AllJobsComplete();
|
||||||
|
void JobOutputName(const QString& filename);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool event(QEvent* e);
|
bool event(QEvent* e);
|
||||||
|
@ -86,6 +86,9 @@
|
|||||||
#include "ui/organisedialog.h"
|
#include "ui/organisedialog.h"
|
||||||
#include "ui/organiseerrordialog.h"
|
#include "ui/organiseerrordialog.h"
|
||||||
#include "ui/qtsystemtrayicon.h"
|
#include "ui/qtsystemtrayicon.h"
|
||||||
|
#ifdef HAVE_AUDIOCD
|
||||||
|
#include "ui/ripcd.h"
|
||||||
|
#endif
|
||||||
#include "ui/settingsdialog.h"
|
#include "ui/settingsdialog.h"
|
||||||
#include "ui/systemtrayicon.h"
|
#include "ui/systemtrayicon.h"
|
||||||
#include "ui/trackselectiondialog.h"
|
#include "ui/trackselectiondialog.h"
|
||||||
@ -345,6 +348,11 @@ MainWindow::MainWindow(Application* app,
|
|||||||
connect(ui_->action_shuffle, SIGNAL(triggered()), app_->playlist_manager(), SLOT(ShuffleCurrent()));
|
connect(ui_->action_shuffle, SIGNAL(triggered()), app_->playlist_manager(), SLOT(ShuffleCurrent()));
|
||||||
connect(ui_->action_open_media, SIGNAL(triggered()), SLOT(AddFile()));
|
connect(ui_->action_open_media, SIGNAL(triggered()), SLOT(AddFile()));
|
||||||
connect(ui_->action_open_cd, SIGNAL(triggered()), SLOT(AddCDTracks()));
|
connect(ui_->action_open_cd, SIGNAL(triggered()), SLOT(AddCDTracks()));
|
||||||
|
#ifdef HAVE_AUDIOCD
|
||||||
|
connect(ui_->action_rip_audio_cd, SIGNAL(triggered()), SLOT(OpenRipCD()));
|
||||||
|
#else
|
||||||
|
ui_->action_rip_audio_cd->setVisible(false);
|
||||||
|
#endif
|
||||||
connect(ui_->action_add_file, SIGNAL(triggered()), SLOT(AddFile()));
|
connect(ui_->action_add_file, SIGNAL(triggered()), SLOT(AddFile()));
|
||||||
connect(ui_->action_add_folder, SIGNAL(triggered()), SLOT(AddFolder()));
|
connect(ui_->action_add_folder, SIGNAL(triggered()), SLOT(AddFolder()));
|
||||||
connect(ui_->action_add_stream, SIGNAL(triggered()), SLOT(AddStream()));
|
connect(ui_->action_add_stream, SIGNAL(triggered()), SLOT(AddStream()));
|
||||||
@ -1670,6 +1678,22 @@ void MainWindow::AddStreamAccepted() {
|
|||||||
AddToPlaylist(data);
|
AddToPlaylist(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void MainWindow::OpenRipCD() {
|
||||||
|
#ifdef HAVE_AUDIOCD
|
||||||
|
if (!rip_cd_) {
|
||||||
|
rip_cd_.reset(new RipCD);
|
||||||
|
}
|
||||||
|
if(rip_cd_->CDIOIsValid()) {
|
||||||
|
rip_cd_->show();
|
||||||
|
} else {
|
||||||
|
QMessageBox cdio_fail(QMessageBox::Critical, tr("Error"), tr("Failed reading CD drive"));
|
||||||
|
cdio_fail.exec();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void MainWindow::AddCDTracks() {
|
void MainWindow::AddCDTracks() {
|
||||||
MimeData* data = new MimeData;
|
MimeData* data = new MimeData;
|
||||||
// We are putting empty data, but we specify cdda mimetype to indicate that
|
// We are putting empty data, but we specify cdda mimetype to indicate that
|
||||||
|
@ -68,6 +68,7 @@ class InternetItem;
|
|||||||
class InternetModel;
|
class InternetModel;
|
||||||
class InternetViewContainer;
|
class InternetViewContainer;
|
||||||
class Remote;
|
class Remote;
|
||||||
|
class RipCD;
|
||||||
class Song;
|
class Song;
|
||||||
class SongInfoBase;
|
class SongInfoBase;
|
||||||
class SongInfoView;
|
class SongInfoView;
|
||||||
@ -81,6 +82,7 @@ class WiimotedevShortcuts;
|
|||||||
class Windows7ThumbBar;
|
class Windows7ThumbBar;
|
||||||
class Ui_MainWindow;
|
class Ui_MainWindow;
|
||||||
|
|
||||||
|
|
||||||
class QSortFilterProxyModel;
|
class QSortFilterProxyModel;
|
||||||
|
|
||||||
class MainWindow : public QMainWindow, public PlatformInterface {
|
class MainWindow : public QMainWindow, public PlatformInterface {
|
||||||
@ -217,6 +219,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
void AddFolder();
|
void AddFolder();
|
||||||
void AddStream();
|
void AddStream();
|
||||||
void AddStreamAccepted();
|
void AddStreamAccepted();
|
||||||
|
void OpenRipCD();
|
||||||
void AddCDTracks();
|
void AddCDTracks();
|
||||||
void AddPodcast();
|
void AddPodcast();
|
||||||
|
|
||||||
@ -293,6 +296,9 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
GlobalSearchView* global_search_view_;
|
GlobalSearchView* global_search_view_;
|
||||||
LibraryViewContainer* library_view_;
|
LibraryViewContainer* library_view_;
|
||||||
FileView* file_view_;
|
FileView* file_view_;
|
||||||
|
#ifdef HAVE_AUDIOCD
|
||||||
|
boost::scoped_ptr<RipCD> rip_cd_;
|
||||||
|
#endif
|
||||||
PlaylistListContainer* playlist_list_;
|
PlaylistListContainer* playlist_list_;
|
||||||
InternetViewContainer* internet_view_;
|
InternetViewContainer* internet_view_;
|
||||||
DeviceViewContainer* device_view_container_;
|
DeviceViewContainer* device_view_container_;
|
||||||
|
@ -424,6 +424,7 @@
|
|||||||
</property>
|
</property>
|
||||||
<addaction name="action_open_media"/>
|
<addaction name="action_open_media"/>
|
||||||
<addaction name="action_open_cd"/>
|
<addaction name="action_open_cd"/>
|
||||||
|
<addaction name="action_rip_audio_cd"/>
|
||||||
<addaction name="action_add_podcast"/>
|
<addaction name="action_add_podcast"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_previous_track"/>
|
<addaction name="action_previous_track"/>
|
||||||
@ -474,9 +475,6 @@
|
|||||||
<addaction name="action_hypnotoad"/>
|
<addaction name="action_hypnotoad"/>
|
||||||
<addaction name="action_enterprise"/>
|
<addaction name="action_enterprise"/>
|
||||||
<addaction name="action_kittens"/>
|
<addaction name="action_kittens"/>
|
||||||
<!-- Hide the console
|
|
||||||
<addaction name="action_console"/>
|
|
||||||
-->
|
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menu_tools">
|
<widget class="QMenu" name="menu_tools">
|
||||||
@ -875,6 +873,11 @@
|
|||||||
<string>Ctrl+Shift+T</string>
|
<string>Ctrl+Shift+T</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_rip_audio_cd">
|
||||||
|
<property name="text">
|
||||||
|
<string>Rip audio CD...</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<layoutdefault spacing="6" margin="11"/>
|
<layoutdefault spacing="6" margin="11"/>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
|
388
src/ui/ripcd.cpp
Normal file
388
src/ui/ripcd.cpp
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
/* This file is part of Clementine.
|
||||||
|
Copyright 2014, Andre Siviero <altsiviero@gmail.com>
|
||||||
|
|
||||||
|
Clementine is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Clementine is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ripcd.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "ui_ripcd.h"
|
||||||
|
#include "transcoder/transcoder.h"
|
||||||
|
#include "transcoder/transcoderoptionsdialog.h"
|
||||||
|
#include "ui/iconloader.h"
|
||||||
|
#include "core/logging.h"
|
||||||
|
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QDataStream>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QtDebug>
|
||||||
|
#include <QtConcurrentRun>
|
||||||
|
#include <cdio/cdio.h>
|
||||||
|
#include <tag.h>
|
||||||
|
#include <taglib.h>
|
||||||
|
#include <tfile.h>
|
||||||
|
#include <fileref.h>
|
||||||
|
#include <wavfile.h>
|
||||||
|
#include <tpropertymap.h>
|
||||||
|
#include <tstring.h>
|
||||||
|
#include <tstringlist.h>
|
||||||
|
|
||||||
|
// winspool.h defines this :(
|
||||||
|
#ifdef AddJob
|
||||||
|
# undef AddJob
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
bool ComparePresetsByName(const TranscoderPreset& left,
|
||||||
|
const TranscoderPreset& right) {
|
||||||
|
return left.name_ < right.name_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char kWavHeaderRiffMarker[] = "RIFF";
|
||||||
|
const char kWavFileTypeFormatChunk[] = "WAVEfmt ";
|
||||||
|
const char kWavDataString[] = "data";
|
||||||
|
|
||||||
|
const int kCheckboxColumn = 0;
|
||||||
|
const int kTrackNumberColumn = 1;
|
||||||
|
const int kTrackTitleColumn = 2;
|
||||||
|
|
||||||
|
}
|
||||||
|
const char* RipCD::kSettingsGroup = "Transcoder";
|
||||||
|
const int RipCD::kProgressInterval = 500;
|
||||||
|
const int RipCD::kMaxDestinationItems = 10;
|
||||||
|
|
||||||
|
RipCD::RipCD(QWidget* parent) :
|
||||||
|
QDialog(parent),
|
||||||
|
transcoder_(new Transcoder(this)),
|
||||||
|
queued_(0),
|
||||||
|
finished_success_(0),
|
||||||
|
finished_failed_(0),
|
||||||
|
ui_(new Ui_RipCD)
|
||||||
|
{
|
||||||
|
|
||||||
|
// Init
|
||||||
|
ui_->setupUi(this);
|
||||||
|
cancel_button_ = ui_->button_box->button(QDialogButtonBox::Cancel);
|
||||||
|
|
||||||
|
connect(ui_->ripButton, SIGNAL(clicked()), this, SLOT(ClickedRipButton()));
|
||||||
|
connect(cancel_button_, SIGNAL(clicked()), SLOT(Cancel()));
|
||||||
|
|
||||||
|
connect(transcoder_, SIGNAL(JobComplete(QString, bool)), SLOT(JobComplete(QString, bool)));
|
||||||
|
connect(transcoder_, SIGNAL(AllJobsComplete()), SLOT(AllJobsComplete()));
|
||||||
|
connect(transcoder_, SIGNAL(JobOutputName(QString)),
|
||||||
|
SLOT(AppendOutput(QString)));
|
||||||
|
connect(this, SIGNAL(RippingComplete()), SLOT(ThreadedTranscoding()));
|
||||||
|
connect(this, SIGNAL(SignalUpdateProgress()), SLOT(UpdateProgress()));
|
||||||
|
|
||||||
|
connect(ui_->options, SIGNAL(clicked()), SLOT(Options()));
|
||||||
|
connect(ui_->select, SIGNAL(clicked()), SLOT(AddDestination()));
|
||||||
|
|
||||||
|
setWindowTitle(tr("Rip CD"));
|
||||||
|
|
||||||
|
cdio_ = cdio_open(NULL, DRIVER_UNKNOWN);
|
||||||
|
if(!cdio_) {
|
||||||
|
qLog(Error) << "Failed to read CD drive";
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
i_tracks = cdio_get_num_tracks(cdio_);
|
||||||
|
ui_->tableWidget->setRowCount(i_tracks);
|
||||||
|
for (int i = 1; i <= i_tracks; i++) {
|
||||||
|
QCheckBox *checkbox_i = new QCheckBox(ui_->tableWidget);
|
||||||
|
checkbox_i->setCheckState(Qt::Checked);
|
||||||
|
checkboxes_.append(checkbox_i);
|
||||||
|
ui_->tableWidget->setCellWidget(i - 1, kCheckboxColumn, checkbox_i);
|
||||||
|
ui_->tableWidget->setCellWidget(i - 1, kTrackNumberColumn,
|
||||||
|
new QLabel(QString::number(i)));
|
||||||
|
QString track_title = QString("Track %1").arg(i);
|
||||||
|
QLineEdit *line_edit_track_title_i = new QLineEdit(track_title,
|
||||||
|
ui_->tableWidget);
|
||||||
|
track_names_.append(line_edit_track_title_i);
|
||||||
|
ui_->tableWidget->setCellWidget(i - 1, kTrackTitleColumn,
|
||||||
|
line_edit_track_title_i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Get presets
|
||||||
|
QList <TranscoderPreset> presets = Transcoder::GetAllPresets();
|
||||||
|
qSort(presets.begin(), presets.end(), ComparePresetsByName);
|
||||||
|
for(const TranscoderPreset& preset : presets) {
|
||||||
|
ui_->format->addItem(
|
||||||
|
QString("%1 (.%2)").arg(preset.name_, preset.extension_),
|
||||||
|
QVariant::fromValue(preset));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load settings
|
||||||
|
QSettings s;
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
last_add_dir_ = s.value("last_add_dir", QDir::homePath()).toString();
|
||||||
|
|
||||||
|
QString last_output_format = s.value("last_output_format", "ogg").toString();
|
||||||
|
for (int i = 0; i < ui_->format->count(); ++i) {
|
||||||
|
if (last_output_format
|
||||||
|
== ui_->format->itemData(i).value<TranscoderPreset>().extension_) {
|
||||||
|
ui_->format->setCurrentIndex(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_->progress_bar->setValue(0);
|
||||||
|
ui_->progress_bar->setMaximum(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
RipCD::~RipCD() {
|
||||||
|
delete ui_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* WAV Header documentation
|
||||||
|
* as taken from:
|
||||||
|
* http://www.topherlee.com/software/pcm-tut-wavformat.html
|
||||||
|
* Pos Value Description
|
||||||
|
* 0-3 | "RIFF" | Marks the file as a riff file.
|
||||||
|
* | Characters are each 1 byte long.
|
||||||
|
* 4-7 | File size (integer) | Size of the overall file - 8 bytes,
|
||||||
|
* | in bytes (32-bit integer).
|
||||||
|
* 8-11 | "WAVE" | File Type Header. For our purposes,
|
||||||
|
* | it always equals "WAVE".
|
||||||
|
* 13-16 | "fmt " | Format chunk marker. Includes trailing null.
|
||||||
|
* 17-20 | 16 | Length of format data as listed above
|
||||||
|
* 21-22 | 1 | Type of format (1 is PCM) - 2 byte integer
|
||||||
|
* 23-24 | 2 | Number of Channels - 2 byte integer
|
||||||
|
* 25-28 | 44100 | Sample Rate - 32 byte integer. Common values
|
||||||
|
* | are 44100 (CD), 48000 (DAT).
|
||||||
|
* | Sample Rate = Number of Samples per second, or Hertz.
|
||||||
|
* 29-32 | 176400 | (Sample Rate * BitsPerSample * Channels) / 8.
|
||||||
|
* 33-34 | 4 | (BitsPerSample * Channels) / 8.1 - 8 bit mono2 - 8 bit stereo/16 bit mono4 - 16 bit stereo
|
||||||
|
* 35-36 | 16 | Bits per sample
|
||||||
|
* 37-40 | "data" | "data" chunk header.
|
||||||
|
* | Marks the beginning of the data section.
|
||||||
|
* 41-44 | File size (data) | Size of the data section.
|
||||||
|
*/
|
||||||
|
void RipCD::WriteWAVHeader(QFile *stream, int32_t i_bytecount) {
|
||||||
|
QDataStream data_stream(stream);
|
||||||
|
data_stream.setByteOrder(QDataStream::LittleEndian);
|
||||||
|
// sizeof() - 1 to avoid including "\0" in the file too
|
||||||
|
data_stream.writeRawData(kWavHeaderRiffMarker,sizeof(kWavHeaderRiffMarker)-1); /* 0-3 */
|
||||||
|
data_stream << qint32(i_bytecount + 44 - 8); /* 4-7 */
|
||||||
|
data_stream.writeRawData(kWavFileTypeFormatChunk,sizeof(kWavFileTypeFormatChunk)-1); /* 8-15 */
|
||||||
|
data_stream << (qint32)16; /* 16-19 */
|
||||||
|
data_stream << (qint16)1; /* 20-21 */
|
||||||
|
data_stream << (qint16)2; /* 22-23 */
|
||||||
|
data_stream << (qint32)44100; /* 24-27 */
|
||||||
|
data_stream << (qint32)(44100 * 2 * 2); /* 28-31 */
|
||||||
|
data_stream << (qint16)4; /* 32-33 */
|
||||||
|
data_stream << (qint16)16; /* 34-35 */
|
||||||
|
data_stream.writeRawData(kWavDataString,sizeof(kWavDataString)-1); /* 36-39 */
|
||||||
|
data_stream << (qint32)i_bytecount; /* 40-43 */
|
||||||
|
}
|
||||||
|
|
||||||
|
int RipCD::NumTracksToRip() {
|
||||||
|
int k = 0;
|
||||||
|
for (int i = 0; i < checkboxes_.length(); i++) {
|
||||||
|
if (checkboxes_.value(i)->isChecked()) {
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCD::ThreadClickedRipButton() {
|
||||||
|
|
||||||
|
QString source_directory = QDir::tempPath() + "/";
|
||||||
|
|
||||||
|
finished_success_ = 0;
|
||||||
|
finished_failed_ = 0;
|
||||||
|
ui_->progress_bar->setMaximum(NumTracksToRip() * 2 * 100);
|
||||||
|
|
||||||
|
// Set up progress bar
|
||||||
|
emit(SignalUpdateProgress());
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 1; i <= i_tracks; i++) {
|
||||||
|
if (!checkboxes_.value(i - 1)->isChecked()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tracks_to_rip_.append(i);
|
||||||
|
|
||||||
|
QString filename = source_directory
|
||||||
|
+ ParseFileFormatString(ui_->format_filename->text(), i) + ".wav";
|
||||||
|
QFile *destination_file = new QFile(filename);
|
||||||
|
destination_file->open(QIODevice::WriteOnly);
|
||||||
|
|
||||||
|
lsn_t i_first_lsn = cdio_get_track_lsn(cdio_, i);
|
||||||
|
lsn_t i_last_lsn = cdio_get_track_last_lsn(cdio_, i);
|
||||||
|
WriteWAVHeader(destination_file,
|
||||||
|
(i_last_lsn - i_first_lsn + 1) * CDIO_CD_FRAMESIZE_RAW);
|
||||||
|
|
||||||
|
QByteArray buffered_input_bytes(CDIO_CD_FRAMESIZE_RAW,'\0');
|
||||||
|
for (lsn_t i_cursor = i_first_lsn; i_cursor <= i_last_lsn; i_cursor++) {
|
||||||
|
if(cdio_read_audio_sector(cdio_, buffered_input_bytes.data(), i_cursor) == DRIVER_OP_SUCCESS) {
|
||||||
|
destination_file->write(buffered_input_bytes.data(), buffered_input_bytes.size());
|
||||||
|
} else {
|
||||||
|
qLog(Error) << "CD read error";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finished_success_++;
|
||||||
|
emit(SignalUpdateProgress());
|
||||||
|
TranscoderPreset preset =
|
||||||
|
ui_->format->itemData(ui_->format->currentIndex())
|
||||||
|
.value<TranscoderPreset>();
|
||||||
|
|
||||||
|
QString outfilename = GetOutputFileName(filename, preset);
|
||||||
|
transcoder_->AddJob(filename.toUtf8().constData(), preset, outfilename);
|
||||||
|
}
|
||||||
|
emit(RippingComplete());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the rightmost non-empty part of 'path'.
|
||||||
|
QString RipCD::TrimPath(const QString& path) const {
|
||||||
|
return path.section('/', -1, -1, QString::SectionSkipEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RipCD::GetOutputFileName(const QString& input,
|
||||||
|
const TranscoderPreset &preset) const {
|
||||||
|
QString path =
|
||||||
|
ui_->destination->itemData(ui_->destination->currentIndex()).toString();
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
// Keep the original path.
|
||||||
|
return input.section('.', 0, -2) + '.' + preset.extension_;
|
||||||
|
} else {
|
||||||
|
QString file_name = TrimPath(input);
|
||||||
|
file_name = file_name.section('.', 0, -2);
|
||||||
|
return path + '/' + file_name + '.' + preset.extension_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RipCD::ParseFileFormatString(const QString& file_format,
|
||||||
|
int track_no) const {
|
||||||
|
QString to_return = file_format;
|
||||||
|
to_return.replace(QString("%artist%"), ui_->artistLineEdit->text());
|
||||||
|
to_return.replace(QString("%album%"), ui_->albumLineEdit->text());
|
||||||
|
to_return.replace(QString("%genre%"), ui_->genreLineEdit->text());
|
||||||
|
to_return.replace(QString("%year%"), ui_->yearLineEdit->text());
|
||||||
|
to_return.replace(QString("%tracknum%"), QString::number(track_no));
|
||||||
|
to_return.replace(QString("%track%"),
|
||||||
|
track_names_.value(track_no - 1)->text());
|
||||||
|
return to_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCD::UpdateProgress() {
|
||||||
|
int progress = (finished_success_ + finished_failed_) * 100;
|
||||||
|
QMap<QString, float> current_jobs = transcoder_->GetProgress();
|
||||||
|
for (float value : current_jobs.values()) {
|
||||||
|
progress += qBound(0, static_cast<int>(value * 100), 99);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_->progress_bar->setValue(progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCD::ThreadedTranscoding() {
|
||||||
|
transcoder_->Start();
|
||||||
|
TranscoderPreset preset =
|
||||||
|
ui_->format->itemData(ui_->format->currentIndex())
|
||||||
|
.value<TranscoderPreset>();
|
||||||
|
// Save the last output format
|
||||||
|
QSettings s;
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
s.setValue("last_output_format", preset.extension_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCD::ClickedRipButton() {
|
||||||
|
QtConcurrent::run(this, &RipCD::ThreadClickedRipButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCD::JobComplete(const QString& filename, bool success) {
|
||||||
|
(*(success ? &finished_success_ : &finished_failed_))++;
|
||||||
|
emit(SignalUpdateProgress());
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCD::AllJobsComplete() {
|
||||||
|
// having a little trouble on wav files, works fine on ogg-vorbis
|
||||||
|
qSort(generated_files_);
|
||||||
|
|
||||||
|
for (int i = 0; i < generated_files_.length(); i++) {
|
||||||
|
TagLib::FileRef f(generated_files_.value(i).toUtf8().constData());
|
||||||
|
|
||||||
|
f.tag()->setTitle(
|
||||||
|
track_names_.value(tracks_to_rip_.value(i) - 1)
|
||||||
|
->text().toUtf8().constData());
|
||||||
|
f.tag()->setAlbum(ui_->albumLineEdit->text().toUtf8().constData());
|
||||||
|
f.tag()->setArtist(ui_->artistLineEdit->text().toUtf8().constData());
|
||||||
|
f.tag()->setGenre(ui_->genreLineEdit->text().toUtf8().constData());
|
||||||
|
f.tag()->setYear(ui_->yearLineEdit->text().toInt());
|
||||||
|
f.tag()->setTrack(tracks_to_rip_.value(i) - 1);
|
||||||
|
// Need to check this
|
||||||
|
// f.tag()->setDisc(ui_->discLineEdit->text().toInt());
|
||||||
|
f.save();
|
||||||
|
}
|
||||||
|
// Resets lists
|
||||||
|
generated_files_.clear();
|
||||||
|
tracks_to_rip_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCD::AppendOutput(const QString& filename) {
|
||||||
|
generated_files_.append(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCD::Options() {
|
||||||
|
TranscoderPreset preset =
|
||||||
|
ui_->format->itemData(
|
||||||
|
ui_->format->currentIndex())
|
||||||
|
.value<TranscoderPreset>();
|
||||||
|
|
||||||
|
TranscoderOptionsDialog dialog(preset.type_, this);
|
||||||
|
if (dialog.is_valid()) {
|
||||||
|
dialog.exec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a folder to the destination box.
|
||||||
|
void RipCD::AddDestination() {
|
||||||
|
int index = ui_->destination->currentIndex();
|
||||||
|
QString initial_dir = (
|
||||||
|
!ui_->destination->itemData(index).isNull() ?
|
||||||
|
ui_->destination->itemData(index).toString() : QDir::homePath());
|
||||||
|
QString dir = QFileDialog::getExistingDirectory(this, tr("Add folder"),
|
||||||
|
initial_dir);
|
||||||
|
|
||||||
|
if (!dir.isEmpty()) {
|
||||||
|
// Keep only a finite number of items in the box.
|
||||||
|
while (ui_->destination->count() >= kMaxDestinationItems) {
|
||||||
|
ui_->destination->removeItem(1); // The oldest folder item.
|
||||||
|
}
|
||||||
|
|
||||||
|
QIcon icon = IconLoader::Load("folder");
|
||||||
|
QVariant data = QVariant::fromValue(dir);
|
||||||
|
// Do not insert duplicates.
|
||||||
|
int duplicate_index = ui_->destination->findData(data);
|
||||||
|
if (duplicate_index == -1) {
|
||||||
|
ui_->destination->addItem(icon, dir, data);
|
||||||
|
ui_->destination->setCurrentIndex(ui_->destination->count() - 1);
|
||||||
|
} else {
|
||||||
|
ui_->destination->setCurrentIndex(duplicate_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCD::Cancel() {
|
||||||
|
transcoder_->Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RipCD::CDIOIsValid() const {
|
||||||
|
return (cdio_);
|
||||||
|
}
|
82
src/ui/ripcd.h
Normal file
82
src/ui/ripcd.h
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/* This file is part of Clementine.
|
||||||
|
Copyright 2014, Andre Siviero <altsiviero@gmail.com>
|
||||||
|
|
||||||
|
Clementine is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Clementine is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SRC_UI_RIPCD_H_
|
||||||
|
#define SRC_UI_RIPCD_H_
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QThread>
|
||||||
|
#include <QFile>
|
||||||
|
#include <cdio/cdio.h>
|
||||||
|
#include "ui_ripcd.h"
|
||||||
|
|
||||||
|
class Ui_RipCD;
|
||||||
|
class Transcoder;
|
||||||
|
|
||||||
|
struct TranscoderPreset;
|
||||||
|
|
||||||
|
class RipCD: public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit RipCD(QWidget* parent = 0);
|
||||||
|
~RipCD();
|
||||||
|
bool CDIOIsValid() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static const char* kSettingsGroup;
|
||||||
|
static const int kProgressInterval;
|
||||||
|
static const int kMaxDestinationItems;
|
||||||
|
Transcoder* transcoder_;
|
||||||
|
int queued_;
|
||||||
|
int finished_success_;
|
||||||
|
int finished_failed_;
|
||||||
|
track_t i_tracks;
|
||||||
|
Ui_RipCD* ui_;
|
||||||
|
CdIo_t *cdio_;
|
||||||
|
QList<QCheckBox*> checkboxes_;
|
||||||
|
QList<QString> generated_files_;
|
||||||
|
QList<int> tracks_to_rip_;
|
||||||
|
QList<QLineEdit*> track_names_;
|
||||||
|
QString last_add_dir_;
|
||||||
|
QPushButton* cancel_button_;
|
||||||
|
|
||||||
|
void WriteWAVHeader(QFile *stream, int32_t i_bytecount);
|
||||||
|
int NumTracksToRip();
|
||||||
|
void ThreadClickedRipButton();
|
||||||
|
QString TrimPath(const QString& path) const;
|
||||||
|
QString GetOutputFileName(const QString& input,
|
||||||
|
const TranscoderPreset& preset) const;
|
||||||
|
QString ParseFileFormatString(const QString& file_format, int track_no) const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void RippingComplete();
|
||||||
|
void SignalUpdateProgress();
|
||||||
|
private slots:
|
||||||
|
void UpdateProgress();
|
||||||
|
void ThreadedTranscoding();
|
||||||
|
void ClickedRipButton();
|
||||||
|
void JobComplete(const QString& filename, bool success);
|
||||||
|
void AllJobsComplete();
|
||||||
|
void AppendOutput(const QString& filename);
|
||||||
|
void Options();
|
||||||
|
void AddDestination();
|
||||||
|
void Cancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SRC_UI_RIPCD_H_
|
302
src/ui/ripcd.ui
Normal file
302
src/ui/ripcd.ui
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>RipCD</class>
|
||||||
|
<widget class="QDialog" name="RipCD">
|
||||||
|
<property name="windowModality">
|
||||||
|
<enum>Qt::NonModal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>601</width>
|
||||||
|
<height>575</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Dialog</string>
|
||||||
|
</property>
|
||||||
|
<widget class="QDialogButtonBox" name="button_box">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>240</x>
|
||||||
|
<y>530</y>
|
||||||
|
<width>341</width>
|
||||||
|
<height>32</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="gridLayoutWidget">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>10</x>
|
||||||
|
<y>197</y>
|
||||||
|
<width>571</width>
|
||||||
|
<height>101</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,0">
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item row="2" column="3" alignment="Qt::AlignLeft">
|
||||||
|
<widget class="QLabel" name="year_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Year</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="5">
|
||||||
|
<widget class="QLabel" name="disc_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Disc</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="album_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Album</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1" colspan="6">
|
||||||
|
<widget class="QLineEdit" name="albumLineEdit"/>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="6">
|
||||||
|
<widget class="QLineEdit" name="discLineEdit">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="4" alignment="Qt::AlignLeft">
|
||||||
|
<widget class="QLineEdit" name="yearLineEdit"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="artist_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Artist</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1" colspan="2">
|
||||||
|
<widget class="QLineEdit" name="genreLineEdit"/>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" alignment="Qt::AlignLeft">
|
||||||
|
<widget class="QLabel" name="genre_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Genre</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1" colspan="6">
|
||||||
|
<widget class="QLineEdit" name="artistLineEdit"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="gridLayoutWidget_2">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>10</x>
|
||||||
|
<y>10</y>
|
||||||
|
<width>571</width>
|
||||||
|
<height>171</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_3">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QTableWidget" name="tableWidget">
|
||||||
|
<property name="columnCount">
|
||||||
|
<number>4</number>
|
||||||
|
</property>
|
||||||
|
<attribute name="horizontalHeaderVisible">
|
||||||
|
<bool>true</bool>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||||
|
<number>10</number>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="verticalHeaderVisible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="verticalHeaderShowSortIndicator" stdset="0">
|
||||||
|
<bool>false</bool>
|
||||||
|
</attribute>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Track</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Title</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Duration</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QPushButton" name="ripButton">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>10</x>
|
||||||
|
<y>480</y>
|
||||||
|
<width>98</width>
|
||||||
|
<height>27</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>&Rip</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QProgressBar" name="progress_bar">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>120</x>
|
||||||
|
<y>480</y>
|
||||||
|
<width>455</width>
|
||||||
|
<height>25</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QGroupBox" name="output_group">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>10</x>
|
||||||
|
<y>320</y>
|
||||||
|
<width>571</width>
|
||||||
|
<height>131</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="title">
|
||||||
|
<string>Output options</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Audio format</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QComboBox" name="format">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QPushButton" name="options">
|
||||||
|
<property name="text">
|
||||||
|
<string>Options...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>Destination</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QComboBox" name="destination">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Alongside the originals</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QPushButton" name="select">
|
||||||
|
<property name="text">
|
||||||
|
<string>Select...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>File Format</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1" colspan="2">
|
||||||
|
<widget class="QLineEdit" name="format_filename">
|
||||||
|
<property name="text">
|
||||||
|
<string>%tracknum% - %artist% - %track%</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>button_box</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>RipCD</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>button_box</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>RipCD</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
Loading…
x
Reference in New Issue
Block a user