diff --git a/article_maker.cc b/article_maker.cc index b192d368..a2afa205 100644 --- a/article_maker.cc +++ b/article_maker.cc @@ -486,7 +486,7 @@ void ArticleRequest::bodyFinished() head += string( "
" + + + "/dicticon.png\">" + Html::escape( tr( "From " ).toUtf8().data() ) + "" + Html::escape( activeDict->getName().c_str() ) + "
"; diff --git a/articleview.cc b/articleview.cc index e5f16f17..ca09d254 100644 --- a/articleview.cc +++ b/articleview.cc @@ -22,6 +22,8 @@ #include #include +#include + #ifdef Q_OS_WIN32 #include @@ -952,10 +954,14 @@ void ArticleView::openLink( QUrl const & url, QUrl const & ref, } } -void ArticleView::saveResource( const QUrl & url, QUrl const & ref ) +vector< ResourceToSaveHandler * > ArticleView::saveResource( const QUrl & url, const QString & fileName ) { - resourceToSaveDownloadRequests.clear(); - resourceToSaveUrl = url; + return saveResource( url, ui.definition->url(), fileName ); +} + +vector< ResourceToSaveHandler * > ArticleView::saveResource( const QUrl & url, const QUrl & ref, const QString & fileName ) +{ + vector< ResourceToSaveHandler * > handlers; sptr< Dictionary::DataRequest > req; if( url.scheme() == "bres" || url.scheme() == "gico" || url.scheme() == "gdau") @@ -989,25 +995,8 @@ void ArticleView::saveResource( const QUrl & url, QUrl const & ref ) req = (*activeDicts)[ x ]->getResource( url.path().mid( 1 ).toUtf8().data() ); - if ( req->isFinished() && req->dataSize() >= 0 ) - { - // A request was instantly finished with success. - // If we've managed to spawn some lingering requests already, - // erase them. - resourceToSaveDownloadRequests.clear(); - - // Handle the result - resourceToSaveDownloadRequests.push_back( req ); - resourceToSaveDownloadFinished(); - return; - } - else - if ( !req->isFinished() ) - { - resourceToSaveDownloadRequests.push_back( req ); - connect( req.get(), SIGNAL( finished() ), - this, SLOT( resourceToSaveDownloadFinished() ), Qt::QueuedConnection ); - } + ResourceToSaveHandler * handler = new ResourceToSaveHandler( this, req, fileName ); + handlers.push_back( handler ); } } } @@ -1016,154 +1005,27 @@ void ArticleView::saveResource( const QUrl & url, QUrl const & ref ) // Normal resource download QString contentType; req = articleNetMgr.getResource( url, contentType ); + + ResourceToSaveHandler * handler = new ResourceToSaveHandler( this, req, fileName ); + handlers.push_back( handler ); } } else + { req = new Dictionary::WebMultimediaDownload( url, articleNetMgr ); - if( url.host().compare( "search" ) != 0 ) - { - if ( !req.get() ) - { - // Request failed, fail - } - else - if ( req->isFinished() && req->dataSize() >= 0 ) - { - // Have data ready, handle it - resourceToSaveDownloadRequests.push_back( req ); - resourceToSaveDownloadFinished(); - return; - } - else - if ( !req->isFinished() ) - { - // Queue to be handled when done - - resourceToSaveDownloadRequests.push_back( req ); - connect( req.get(), SIGNAL( finished() ), - this, SLOT( resourceToSaveDownloadFinished() ), Qt::QueuedConnection ); - } + ResourceToSaveHandler * handler = new ResourceToSaveHandler( this, req, fileName ); + handlers.push_back( handler ); } - if ( resourceToSaveDownloadRequests.empty() ) // No requests were queued + if ( handlers.empty() ) // No requests were queued { emit statusBarMessage( tr( "ERROR: %1" ).arg( tr( "The referenced resource doesn't exist." ) ), 10000, QPixmap( ":/icons/error.png" ) ); - return; - } - else - resourceToSaveDownloadFinished(); // Check any requests finished already -} - -void ArticleView::resourceToSaveDownloadFinished() -{ - if ( resourceToSaveDownloadRequests.empty() ) - return; // Stray signal - - QByteArray resourceData; - - // Find any finished resources - - for( list< sptr< Dictionary::DataRequest > >::iterator i = - resourceToSaveDownloadRequests.begin(); i != resourceToSaveDownloadRequests.end(); ) - { - if ( (*i)->isFinished() ) - { - if ( (*i)->dataSize() >= 0 ) - { - // Ok, got one finished, all others are irrelevant now - - vector< char > const & data = (*i)->getFullData(); - resourceData = QByteArray( data.data(), data.size() ); - break; - } - else - { - // This one had no data. Erase it. - resourceToSaveDownloadRequests.erase( i++ ); - } - } - else // Unfinished, try the next one. - i++; } - if( !resourceData.isEmpty() ) - { - // Resource found, clear all requests - resourceToSaveDownloadRequests.clear(); - - QString fileName; - QString savePath; - if( cfg.resourceSavePath.isEmpty() ) - savePath = QDir::homePath(); - else - { - savePath = QDir::fromNativeSeparators( cfg.resourceSavePath ); - if( !QDir( savePath ).exists() ) - savePath = QDir::homePath(); - } - - QString name = resourceToSaveUrl.path().section( '/', -1 ); - - if ( resourceToSaveUrl.scheme() == "gdau" || - Dictionary::WebMultimediaDownload::isAudioUrl( resourceToSaveUrl ) ) - { - // Audio data - if( name.indexOf( '.' ) < 0 ) - name += ".wav"; - - fileName = savePath + "/" + name; - fileName = QFileDialog::getSaveFileName( parentWidget(), tr( "Save sound" ), - fileName, - tr( "Sound files (*.wav *.ogg *.mp3 *.mp4 *.aac *.flac *.mid *.wv *.ape);;All files (*.*)" ) ); - } - else - { - // Image data - - // Check for babylon image name - if( name[ 0 ] == '\x1E' ) - name.remove( 0, 1 ); - if( name[ name.length() - 1 ] == '\x1F' ) - name.chop( 1 ); - - fileName = savePath + "/" + name; - fileName = QFileDialog::getSaveFileName( parentWidget(), tr( "Save image" ), - fileName, - tr( "Image files (*.bmp *.jpg *.png *.tif);;All files (*.*)" ) ); - } - - // Write data to file - - if( !fileName.isEmpty() ) - { - QFileInfo fileInfo( fileName ); - emit storeResourceSavePath( QDir::toNativeSeparators( fileInfo.absoluteDir().absolutePath() ) ); - QFile file( fileName ); - if ( file.open( QFile::WriteOnly ) ) - { - file.write( resourceData.data(), resourceData.size() ); - file.close(); - } - if( file.error() ) - { - emit statusBarMessage( - tr( "ERROR: %1" ).arg( tr( "Resource saving error: " ) + file.errorString() ), - 10000, QPixmap( ":/icons/error.png" ) ); - } - } - - return; - } - - if ( resourceToSaveDownloadRequests.empty() ) - { - emit statusBarMessage( - tr( "ERROR: %1" ).arg( tr( "The referenced resource failed to download." ) ), - 10000, QPixmap( ":/icons/error.png" ) ); - } + return handlers; } void ArticleView::updateMutedContents() @@ -1499,11 +1361,57 @@ void ArticleView::contextMenuRequested( QPoint const & pos ) emit showDefinitionInNewTab( selectedText, groupComboBox->getCurrentGroup(), QString(), Contexts() ); else - if( result == saveImageAction ) - saveResource( imageUrl, ui.definition->url() ); - else - if( result == saveSoundAction ) - saveResource( targetUrl, ui.definition->url() ); + if( result == saveImageAction || result == saveSoundAction ) + { + QUrl url = ( result == saveImageAction ) ? imageUrl : targetUrl; + QString savePath; + QString fileName; + + if ( cfg.resourceSavePath.isEmpty() ) + savePath = QDir::homePath(); + else + { + savePath = QDir::fromNativeSeparators( cfg.resourceSavePath ); + if ( !QDir( savePath ).exists() ) + savePath = QDir::homePath(); + } + + QString name = url.path().section( '/', -1 ); + + if ( result == saveSoundAction ) + { + // Audio data + if ( name.indexOf( '.' ) < 0 ) + name += ".wav"; + + fileName = savePath + "/" + name; + fileName = QFileDialog::getSaveFileName( parentWidget(), tr( "Save sound" ), + fileName, + tr( "Sound files (*.wav *.ogg *.mp3 *.mp4 *.aac *.flac *.mid *.wv *.ape);;All files (*.*)" ) ); + } + else + { + // Image data + + // Check for babylon image name + if ( name[ 0 ] == '\x1E' ) + name.remove( 0, 1 ); + if ( name.length() && name[ name.length() - 1 ] == '\x1F' ) + name.chop( 1 ); + + fileName = savePath + "/" + name; + fileName = QFileDialog::getSaveFileName( parentWidget(), tr( "Save image" ), + fileName, + tr( "Image files (*.bmp *.jpg *.png *.tif);;All files (*.*)" ) ); + } + + if ( !fileName.isEmpty() ) + { + QFileInfo fileInfo( fileName ); + emit storeResourceSavePath( QDir::toNativeSeparators( fileInfo.absoluteDir().absolutePath() ) ); + saveResource( url, ui.definition->url(), fileName ); + } + } else { if ( !popupView && result == maxDictionaryRefsAction ) @@ -2147,3 +2055,68 @@ QString ArticleView::wordAtPoint( int x, int y ) } #endif + +ResourceToSaveHandler::ResourceToSaveHandler( + ArticleView * view, sptr< Dictionary::DataRequest > req, + QString const & fileName ) : + QObject( view ), + req( req ), + fileName( fileName ) +{ + connect( this, SIGNAL( statusBarMessage( QString, int, QPixmap ) ), + view, SIGNAL( statusBarMessage( QString, int, QPixmap ) ) ); + + // If DataRequest finsihed immediately, call our handler directly + if ( req.get()->isFinished() ) + { + QMetaObject::invokeMethod( this, "downloadFinished", Qt::QueuedConnection ); + } + else + { + connect( req.get(), SIGNAL( finished() ), this, SLOT( downloadFinished() ) ); + } +} + +void ResourceToSaveHandler::downloadFinished() +{ + assert( req && req.get()->isFinished() ); + + QByteArray resourceData; + + if ( req.get()->dataSize() >= 0 ) + { + vector< char > const & data = req.get()->getFullData(); + resourceData = QByteArray( data.data(), data.size() ); + } + + // Write data to file + + if ( !resourceData.isEmpty() && !fileName.isEmpty() ) + { + QFileInfo fileInfo( fileName ); + QDir().mkpath( fileInfo.absoluteDir().absolutePath() ); + + QFile file( fileName ); + if ( file.open( QFile::WriteOnly ) ) + { + file.write( resourceData.data(), resourceData.size() ); + file.close(); + } + + if ( file.error() ) + { + emit statusBarMessage( + tr( "ERROR: %1" ).arg( tr( "Resource saving error: " ) + file.errorString() ), + 10000, QPixmap( ":/icons/error.png" ) ); + } + } + else + { + emit statusBarMessage( + tr( "ERROR: %1" ).arg( tr( "The referenced resource failed to download." ) ), + 10000, QPixmap( ":/icons/error.png" ) ); + } + + emit done(); + deleteLater(); +} diff --git a/articleview.hh b/articleview.hh index 7e0ab67a..3fcc3653 100644 --- a/articleview.hh +++ b/articleview.hh @@ -13,6 +13,9 @@ #include "groupcombobox.hh" #include "ui_articleview.h" + +class ResourceToSaveHandler; + /// A widget with the web view tailored to view and handle articles -- it /// uses the appropriate netmgr, handles link clicks, rmb clicks etc class ArticleView: public QFrame @@ -42,9 +45,6 @@ class ArticleView: public QFrame /// Url of the resourceDownloadRequests QUrl resourceDownloadUrl; - std::list< sptr< Dictionary::DataRequest > > resourceToSaveDownloadRequests; - QUrl resourceToSaveUrl; - /// For resources opened via desktop services QString desktopOpenedTempFile; @@ -162,6 +162,9 @@ public: /// Returns the dictionary id of the currently active article in the view. QString getActiveArticleId(); + std::vector< ResourceToSaveHandler * > saveResource( const QUrl & url, const QString & fileName ); + std::vector< ResourceToSaveHandler * > saveResource( const QUrl & url, const QUrl & ref, const QString & fileName ); + signals: void iconChanged( ArticleView *, QIcon const & icon ); @@ -238,7 +241,6 @@ private slots: void contextMenuRequested( QPoint const & ); void resourceDownloadFinished(); - void resourceToSaveDownloadFinished(); /// We handle pasting by attempting to define the word in clipboard. void pasteTriggered(); @@ -315,8 +317,6 @@ private: /// for the given group. If there are none, returns empty string. QString getMutedForGroup( unsigned group ); - void saveResource( QUrl const &, QUrl const & ); - protected: // We need this to hide the search bar when we're showed @@ -336,4 +336,24 @@ public: }; +class ResourceToSaveHandler: public QObject +{ + Q_OBJECT + +public: + explicit ResourceToSaveHandler( ArticleView * view, sptr< Dictionary::DataRequest > req, + QString const & fileName ); + +signals: + void done(); + void statusBarMessage( QString const & message, int timeout = 0, QPixmap const & pixmap = QPixmap() ); + +private slots: + void downloadFinished(); + +private: + sptr< Dictionary::DataRequest > req; + QString fileName; +}; + #endif diff --git a/mainwindow.cc b/mainwindow.cc index 347df6e1..78a62fe3 100644 --- a/mainwindow.cc +++ b/mainwindow.cc @@ -24,6 +24,7 @@ #include "fsencoding.hh" #include #include "historypanewidget.hh" +#include #ifdef Q_OS_MAC #include "lionsupport.h" @@ -2879,6 +2880,39 @@ void MainWindow::printPreviewPaintRequested( QPrinter * printer ) view->print( printer ); } +static void filterAndCollectResources( QString & html, QRegExp & rx, const QString & sep, + const QString & folder, set< QByteArray > & resourceIncluded, + vector< pair< QUrl, QString > > & downloadResources ) +{ + int pos = 0; + + while ( ( pos = rx.indexIn( html, pos ) ) != -1 ) + { + QUrl url( rx.cap( 1 ) ); + QString host = url.host(); + QString resourcePath = QString::fromLatin1( QUrl::toPercentEncoding( url.path(), "/" ) ); + + if ( !host.startsWith( '/' ) ) + host.insert( 0, '/' ); + if ( !resourcePath.startsWith( '/' ) ) + resourcePath.insert( 0, '/' ); + + QCryptographicHash hash( QCryptographicHash::Md5 ); + hash.addData( rx.cap().toUtf8() ); + + if ( resourceIncluded.insert( hash.result() ).second ) + { + // Gather resouce information (url, filename) to be download later + downloadResources.push_back( pair( url, folder + host + resourcePath ) ); + } + + // Modify original url, set to the native one + QString newUrl = sep + QDir( folder ).dirName() + host + resourcePath + sep; + html.replace( pos, rx.cap().length(), newUrl ); + pos += newUrl.length(); + } +} + void MainWindow::on_saveArticle_triggered() { ArticleView *view = getCurrentArticleView(); @@ -2895,10 +2929,19 @@ void MainWindow::on_saveArticle_triggered() savePath = QDir::homePath(); } + QFileDialog::Options options = QFileDialog::HideNameFilterDetails; + QString selectedFilter; + QStringList filters; + filters.push_back( tr( "Article, Complete (*.html)" ) ); + filters.push_back( tr( "Article, HTML Only (*.html)" ) ); + fileName = savePath + "/" + fileName; fileName = QFileDialog::getSaveFileName( this, tr( "Save Article As" ), - fileName, - tr( "Html files (*.html *.htm)" ) ); + fileName, + filters.join( ";;" ), + &selectedFilter, options ); + + bool complete = ( selectedFilter == filters[ 0 ] ); if ( !fileName.isEmpty() ) { @@ -2912,8 +2955,51 @@ void MainWindow::on_saveArticle_triggered() } else { - file.write( view->toHtml().toUtf8() ); - cfg.articleSavePath = QDir::toNativeSeparators( QFileInfo( fileName ).absoluteDir().absolutePath() ); + QString html = view->toHtml(); + QFileInfo fi( fileName ); + cfg.articleSavePath = QDir::toNativeSeparators( fi.absoluteDir().absolutePath() ); + + if ( complete ) + { + QString folder = fi.absoluteDir().absolutePath() + "/" + fi.baseName() + "_files"; + QRegExp rx1( "\"((?:bres|gico|gdau|qrcx)://[^\"]+)\"" ); + QRegExp rx2( "'((?:bres|gico|gdau|qrcx)://[^']+)'" ); + set< QByteArray > resourceIncluded; + vector< pair< QUrl, QString > > downloadResources; + + filterAndCollectResources( html, rx1, "\"", folder, resourceIncluded, downloadResources ); + filterAndCollectResources( html, rx2, "'", folder, resourceIncluded, downloadResources ); + + ArticleSaveProgressDialog * progressDialog = new ArticleSaveProgressDialog( this ); + // reserve '1' for saving main html file + int maxVal = 1; + + // Pull and save resources to files + for ( vector< pair< QUrl, QString > >::const_iterator i = downloadResources.begin(); + i != downloadResources.end(); i++ ) + { + vector< ResourceToSaveHandler * > handlerss = view->saveResource( i->first, i->second ); + maxVal += handlerss.size(); + + for ( vector< ResourceToSaveHandler * >::iterator j = handlerss.begin(); + j != handlerss.end(); j++ ) + { + connect( *j, SIGNAL( done() ), progressDialog, SLOT( perform() ) ); + } + } + + progressDialog->setLabelText( tr("Saving article...") ); + progressDialog->setRange( 0, maxVal ); + progressDialog->setValue( 0 ); + progressDialog->show(); + + file.write( html.toUtf8() ); + progressDialog->setValue( 1 ); + } + else + { + file.write( html.toUtf8() ); + } } } } diff --git a/mainwindow.hh b/mainwindow.hh index ec785723..9f743aad 100644 --- a/mainwindow.hh +++ b/mainwindow.hh @@ -9,6 +9,7 @@ #include #include #include +#include #include "ui_mainwindow.h" #include "folding.hh" #include "config.hh" @@ -410,4 +411,29 @@ private slots: #endif }; +class ArticleSaveProgressDialog : public QProgressDialog +{ +Q_OBJECT + +public: + explicit ArticleSaveProgressDialog( QWidget * parent = 0, Qt::WindowFlags f = 0 ): + QProgressDialog( parent, f ) + { + setAutoReset( false ); + setAutoClose( false ); + } + +public slots: + void perform() + { + int progress = value() + 1; + if ( progress == maximum() ) + { + emit close(); + deleteLater(); + } + setValue( progress ); + } +}; + #endif