Add an option to enable saving resource files with articles (#321)

This commit is contained in:
Timon Wong 2013-05-27 19:18:13 +08:00
parent f4734c076c
commit 1ba03a326d
5 changed files with 278 additions and 173 deletions

View file

@ -486,7 +486,7 @@ void ArticleRequest::bodyFinished()
head += string( "<div class=\"gddictname\"><span class=\"gddicticon\"><img src=\"gico://")
+ Html::escape( dictId )
+ "/\"></span><span class=\"gdfromprefix\">" +
+ "/dicticon.png\"></span><span class=\"gdfromprefix\">" +
Html::escape( tr( "From " ).toUtf8().data() ) + "</span>" +
Html::escape( activeDict->getName().c_str() )
+ "</div>";

View file

@ -22,6 +22,8 @@
#include <QWebElement>
#include <QCryptographicHash>
#include <assert.h>
#ifdef Q_OS_WIN32
#include <windows.h>
@ -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();
}

View file

@ -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

View file

@ -24,6 +24,7 @@
#include "fsencoding.hh"
#include <QProcess>
#include "historypanewidget.hh"
#include <QCryptographicHash>
#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<QUrl, QString>( 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() );
}
}
}
}

View file

@ -9,6 +9,7 @@
#include <QToolButton>
#include <QSystemTrayIcon>
#include <QNetworkAccessManager>
#include <QProgressDialog>
#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