Browse Source

Merge pull request #2191 from glassez/webui

WebUI: Implement server-side filtering, sorting and limit/offset.
adaptive-webui-19844
sledgehammer999 10 years ago
parent
commit
3672363207
  1. 87
      src/qtlibtorrent/qtorrenthandle.cpp
  2. 32
      src/qtlibtorrent/qtorrenthandle.h
  3. 129
      src/webui/btjson.cpp
  4. 8
      src/webui/btjson.h
  5. 118
      src/webui/qtorrentfilter.cpp
  6. 63
      src/webui/qtorrentfilter.h
  7. 14
      src/webui/requesthandler.cpp
  8. 6
      src/webui/webui.pri
  9. 6
      src/webui/www/public/filters.html
  10. 216
      src/webui/www/public/scripts/client.js
  11. 258
      src/webui/www/public/scripts/dynamicTable.js
  12. 54
      src/webui/www/public/transferlist.html

87
src/qtlibtorrent/qtorrenthandle.cpp

@ -40,6 +40,7 @@ @@ -40,6 +40,7 @@
#include "preferences.h"
#include "qtorrenthandle.h"
#include "torrentpersistentdata.h"
#include "qbtsession.h"
#include <libtorrent/version.hpp>
#include <libtorrent/magnet_uri.hpp>
#include <libtorrent/torrent_info.hpp>
@ -406,6 +407,51 @@ void QTorrentHandle::file_progress(std::vector<size_type>& fp) const { @@ -406,6 +407,51 @@ void QTorrentHandle::file_progress(std::vector<size_type>& fp) const {
torrent_handle::file_progress(fp, torrent_handle::piece_granularity);
}
QTorrentState QTorrentHandle::torrentState() const {
QTorrentState state = QTorrentState::Unknown;
libtorrent::torrent_status s = status(torrent_handle::query_accurate_download_counters);
if (is_paused(s)) {
if (has_error(s))
state = QTorrentState::Error;
else
state = is_seed(s) ? QTorrentState::PausedUploading : QTorrentState::PausedDownloading;
}
else {
if (QBtSession::instance()->isQueueingEnabled() && is_queued(s)) {
state = is_seed(s) ? QTorrentState::QueuedUploading : QTorrentState::QueuedDownloading;
}
else {
switch (s.state) {
case torrent_status::finished:
case torrent_status::seeding:
state = s.upload_payload_rate > 0 ? QTorrentState::Uploading : QTorrentState::StalledUploading;
break;
case torrent_status::allocating:
case torrent_status::checking_files:
case torrent_status::queued_for_checking:
case torrent_status::checking_resume_data:
state = is_seed(s) ? QTorrentState::CheckingUploading : QTorrentState::CheckingDownloading;
break;
case torrent_status::downloading:
case torrent_status::downloading_metadata:
state = s.download_payload_rate > 0 ? QTorrentState::Downloading : QTorrentState::StalledDownloading;
break;
default:
qWarning("Unrecognized torrent status, should not happen!!! status was %d", this->state());
}
}
}
return state;
}
qulonglong QTorrentHandle::eta() const
{
libtorrent::torrent_status s = status(torrent_handle::query_accurate_download_counters);
return QBtSession::instance()->getETA(hash(), s);
}
//
// Setters
//
@ -681,3 +727,44 @@ QString QTorrentHandle::filepath_at(const libtorrent::torrent_info &info, unsign @@ -681,3 +727,44 @@ QString QTorrentHandle::filepath_at(const libtorrent::torrent_info &info, unsign
return fsutils::fromNativePath(misc::toQStringU(info.files().file_path(index)));
}
QTorrentState::QTorrentState(int value)
: m_value(value)
{
}
QString QTorrentState::toString() const
{
switch (m_value) {
case Error:
return "error";
case Uploading:
return "uploading";
case PausedUploading:
return "pausedUP";
case QueuedUploading:
return "queuedUP";
case StalledUploading:
return "stalledUP";
case CheckingUploading:
return "checkingUP";
case Downloading:
return "downloading";
case PausedDownloading:
return "pausedDL";
case QueuedDownloading:
return "queuedDL";
case StalledDownloading:
return "stalledDL";
case CheckingDownloading:
return "checkingDL";
default:
return "unknown";
}
}
QTorrentState::operator int() const
{
return m_value;
}

32
src/qtlibtorrent/qtorrenthandle.h

@ -39,6 +39,36 @@ QT_BEGIN_NAMESPACE @@ -39,6 +39,36 @@ QT_BEGIN_NAMESPACE
class QStringList;
QT_END_NAMESPACE
class QTorrentState
{
public:
enum {
Unknown = -1,
Error,
Uploading,
PausedUploading,
QueuedUploading,
StalledUploading,
CheckingUploading,
Downloading,
PausedDownloading,
QueuedDownloading,
StalledDownloading,
CheckingDownloading
};
QTorrentState(int value);
operator int() const;
QString toString() const;
private:
int m_value;
};
// A wrapper for torrent_handle in libtorrent
// to interact well with Qt types
class QTorrentHandle : public libtorrent::torrent_handle {
@ -94,6 +124,8 @@ public: @@ -94,6 +124,8 @@ public:
void downloading_pieces(libtorrent::bitfield& bf) const;
bool has_metadata() const;
void file_progress(std::vector<libtorrent::size_type>& fp) const;
QTorrentState torrentState() const;
qulonglong eta() const;
//
// Setters

129
src/webui/btjson.cpp

@ -32,10 +32,14 @@ @@ -32,10 +32,14 @@
#include "fs_utils.h"
#include "qbtsession.h"
#include "torrentpersistentdata.h"
#include "qtorrentfilter.h"
#include "jsonutils.h"
#include <QDebug>
#include <QVariant>
#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
#include <QMetaType>
#endif
#if QT_VERSION >= QT_VERSION_CHECK(4, 7, 0)
#include <QElapsedTimer>
#endif
@ -132,6 +136,58 @@ static const char KEY_TRANSFER_DLDATA[] = "dl_info_data"; @@ -132,6 +136,58 @@ static const char KEY_TRANSFER_DLDATA[] = "dl_info_data";
static const char KEY_TRANSFER_UPSPEED[] = "up_info_speed";
static const char KEY_TRANSFER_UPDATA[] = "up_info_data";
class QTorrentCompare
{
public:
QTorrentCompare(QString key, bool greaterThan = false)
: key_(key)
, greaterThan_(greaterThan)
#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
, type_(QVariant::Invalid)
#endif
{
}
bool operator()(QVariant torrent1, QVariant torrent2)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
if (type_ == QVariant::Invalid)
type_ = torrent1.toMap().value(key_).type();
switch (type_) {
case QVariant::Int:
return greaterThan_ ? torrent1.toMap().value(key_).toInt() > torrent2.toMap().value(key_).toInt()
: torrent1.toMap().value(key_).toInt() < torrent2.toMap().value(key_).toInt();
case QVariant::LongLong:
return greaterThan_ ? torrent1.toMap().value(key_).toLongLong() > torrent2.toMap().value(key_).toLongLong()
: torrent1.toMap().value(key_).toLongLong() < torrent2.toMap().value(key_).toLongLong();
case QVariant::ULongLong:
return greaterThan_ ? torrent1.toMap().value(key_).toULongLong() > torrent2.toMap().value(key_).toULongLong()
: torrent1.toMap().value(key_).toULongLong() < torrent2.toMap().value(key_).toULongLong();
case QMetaType::Float:
return greaterThan_ ? torrent1.toMap().value(key_).toFloat() > torrent2.toMap().value(key_).toFloat()
: torrent1.toMap().value(key_).toFloat() < torrent2.toMap().value(key_).toFloat();
case QVariant::Double:
return greaterThan_ ? torrent1.toMap().value(key_).toDouble() > torrent2.toMap().value(key_).toDouble()
: torrent1.toMap().value(key_).toDouble() < torrent2.toMap().value(key_).toDouble();
default:
return greaterThan_ ? torrent1.toMap().value(key_).toString() > torrent2.toMap().value(key_).toString()
: torrent1.toMap().value(key_).toString() < torrent2.toMap().value(key_).toString();
}
#else
return greaterThan_ ? torrent1.toMap().value(key_) > torrent2.toMap().value(key_)
: torrent1.toMap().value(key_) < torrent2.toMap().value(key_);
#endif
}
private:
QString key_;
bool greaterThan_;
#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
QVariant::Type type_;
#endif
};
static QVariantMap toMap(const QTorrentHandle& h)
{
libtorrent::torrent_status status = h.status(torrent_handle::query_accurate_download_counters);
@ -153,40 +209,8 @@ static QVariantMap toMap(const QTorrentHandle& h) @@ -153,40 +209,8 @@ static QVariantMap toMap(const QTorrentHandle& h)
ret[KEY_TORRENT_NUM_INCOMPLETE] = status.num_incomplete;
const qreal ratio = QBtSession::instance()->getRealRatio(status);
ret[KEY_TORRENT_RATIO] = (ratio > 100.) ? -1 : ratio;
qulonglong eta = 0;
QString state;
if (h.is_paused(status)) {
if (h.has_error(status))
state = "error";
else
state = h.is_seed(status) ? "pausedUP" : "pausedDL";
} else {
if (QBtSession::instance()->isQueueingEnabled() && h.is_queued(status))
state = h.is_seed(status) ? "queuedUP" : "queuedDL";
else {
switch (status.state) {
case torrent_status::finished:
case torrent_status::seeding:
state = status.upload_payload_rate > 0 ? "uploading" : "stalledUP";
break;
case torrent_status::allocating:
case torrent_status::checking_files:
case torrent_status::queued_for_checking:
case torrent_status::checking_resume_data:
state = h.is_seed(status) ? "checkingUP" : "checkingDL";
break;
case torrent_status::downloading:
case torrent_status::downloading_metadata:
state = status.download_payload_rate > 0 ? "downloading" : "stalledDL";
eta = QBtSession::instance()->getETA(h.hash(), status);
break;
default:
qWarning("Unrecognized torrent status, should not happen!!! status was %d", h.state());
}
}
}
ret[KEY_TORRENT_ETA] = eta ? eta : MAX_ETA;
ret[KEY_TORRENT_STATE] = state;
ret[KEY_TORRENT_STATE] = h.torrentState().toString();
ret[KEY_TORRENT_ETA] = h.eta();
return ret;
}
@ -211,15 +235,37 @@ static QVariantMap toMap(const QTorrentHandle& h) @@ -211,15 +235,37 @@ static QVariantMap toMap(const QTorrentHandle& h)
* - "eta": Torrent ETA
* - "state": Torrent state
*/
QByteArray btjson::getTorrents()
QByteArray btjson::getTorrents(QString filter, QString label,
QString sortedColumn, bool reverse, int limit, int offset)
{
CACHED_VARIABLE(QVariantList, torrent_list, CACHE_DURATION_MS);
QVariantList torrent_list;
std::vector<torrent_handle> torrents = QBtSession::instance()->getTorrents();
std::vector<torrent_handle>::const_iterator it = torrents.begin();
std::vector<torrent_handle>::const_iterator end = torrents.end();
QTorrentFilter torrentFilter(filter, label);
for( ; it != end; ++it) {
torrent_list.append(toMap(QTorrentHandle(*it)));
QTorrentHandle torrent = QTorrentHandle(*it);
if (torrentFilter.apply(torrent))
torrent_list.append(toMap(torrent));
}
std::sort(torrent_list.begin(), torrent_list.end(), QTorrentCompare(sortedColumn, reverse));
int size = torrent_list.size();
// normalize offset
if (offset < 0)
offset = size - offset;
if ((offset >= size) || (offset < 0))
offset = 0;
// normalize limit
if (limit <= 0)
limit = -1; // unlimited
if ((limit > 0) || (offset > 0))
return json::toJson(torrent_list.mid(offset, limit));
else
return json::toJson(torrent_list);
}
@ -262,7 +308,8 @@ QByteArray btjson::getTrackersForTorrent(const QString& hash) @@ -262,7 +308,8 @@ QByteArray btjson::getTrackersForTorrent(const QString& hash)
tracker_list.append(tracker_dict);
}
} catch(const std::exception& e) {
}
catch(const std::exception& e) {
qWarning() << Q_FUNC_INFO << "Invalid torrent: " << misc::toQStringU(e.what());
return QByteArray();
}
@ -323,7 +370,8 @@ QByteArray btjson::getPropertiesForTorrent(const QString& hash) @@ -323,7 +370,8 @@ QByteArray btjson::getPropertiesForTorrent(const QString& hash)
data[KEY_PROP_CONNECT_COUNT_LIMIT] = status.connections_limit;
const qreal ratio = QBtSession::instance()->getRealRatio(status);
data[KEY_PROP_RATIO] = ratio > 100. ? -1 : ratio;
} catch(const std::exception& e) {
}
catch(const std::exception& e) {
qWarning() << Q_FUNC_INFO << "Invalid torrent: " << misc::toQStringU(e.what());
return QByteArray();
}
@ -368,7 +416,8 @@ QByteArray btjson::getFilesForTorrent(const QString& hash) @@ -368,7 +416,8 @@ QByteArray btjson::getFilesForTorrent(const QString& hash)
file_list.append(file_dict);
}
} catch (const std::exception& e) {
}
catch (const std::exception& e) {
qWarning() << Q_FUNC_INFO << "Invalid torrent: " << misc::toQStringU(e.what());
return QByteArray();
}

8
src/webui/btjson.h

@ -34,14 +34,18 @@ @@ -34,14 +34,18 @@
#include <QCoreApplication>
#include <QString>
class btjson {
class QTorrentHandle;
class btjson
{
Q_DECLARE_TR_FUNCTIONS(misc)
private:
btjson() {}
public:
static QByteArray getTorrents();
static QByteArray getTorrents(QString filter = "all", QString label = QString(),
QString sortedColumn = "name", bool reverse = false, int limit = 0, int offset = 0);
static QByteArray getTrackersForTorrent(const QString& hash);
static QByteArray getPropertiesForTorrent(const QString& hash);
static QByteArray getFilesForTorrent(const QString& hash);

118
src/webui/qtorrentfilter.cpp

@ -0,0 +1,118 @@ @@ -0,0 +1,118 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru>
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#include "torrentpersistentdata.h"
#include "qtorrentfilter.h"
QTorrentFilter::QTorrentFilter(QString filter, QString label)
: type_(All)
, label_(label)
{
if (filter == "downloading")
type_ = Downloading;
else if (filter == "completed")
type_ = Completed;
else if (filter == "paused")
type_ = Paused;
else if (filter == "active")
type_ = Active;
else if (filter == "inactive")
type_ = Inactive;
}
bool QTorrentFilter::apply(const QTorrentHandle& h) const
{
if (!torrentHasLabel(h))
return false;
switch (type_) {
case Downloading:
return isTorrentDownloading(h);
case Completed:
return isTorrentCompleted(h);
case Paused:
return isTorrentPaused(h);
case Active:
return isTorrentActive(h);
case Inactive:
return isTorrentInactive(h);
default: // All
return true;
}
}
bool QTorrentFilter::isTorrentDownloading(const QTorrentHandle &h) const
{
const QTorrentState state = h.torrentState();
return state == QTorrentState::Downloading
|| state == QTorrentState::StalledDownloading
|| state == QTorrentState::CheckingDownloading
|| state == QTorrentState::PausedDownloading
|| state == QTorrentState::QueuedDownloading;
}
bool QTorrentFilter::isTorrentCompleted(const QTorrentHandle &h) const
{
const QTorrentState state = h.torrentState();
return state == QTorrentState::Uploading
|| state == QTorrentState::StalledUploading
|| state == QTorrentState::CheckingUploading
|| state == QTorrentState::PausedUploading
|| state == QTorrentState::QueuedUploading;
}
bool QTorrentFilter::isTorrentPaused(const QTorrentHandle &h) const
{
const QTorrentState state = h.torrentState();
return state == QTorrentState::PausedDownloading
|| state == QTorrentState::PausedUploading;
}
bool QTorrentFilter::isTorrentActive(const QTorrentHandle &h) const
{
const QTorrentState state = h.torrentState();
return state == QTorrentState::Downloading
|| state == QTorrentState::Uploading;
}
bool QTorrentFilter::isTorrentInactive(const QTorrentHandle &h) const
{
return !isTorrentActive(h);
}
bool QTorrentFilter::torrentHasLabel(const QTorrentHandle &h) const
{
if (label_.isNull())
return true;
else
return TorrentPersistentData::getLabel(h.hash()) == label_;
}

63
src/webui/qtorrentfilter.h

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru>
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#ifndef QTORRENTFILTER_H
#define QTORRENTFILTER_H
#include "qtorrenthandle.h"
class QTorrentFilter
{
public:
enum
{
All,
Downloading,
Completed,
Paused,
Active,
Inactive
};
// label: pass empty string for "no label" or null string (QString()) for "any label"
QTorrentFilter(QString filter, QString label = QString());
bool apply(const QTorrentHandle& h) const;
private:
int type_;
QString label_;
bool isTorrentDownloading(const QTorrentHandle &h) const;
bool isTorrentCompleted(const QTorrentHandle &h) const;
bool isTorrentPaused(const QTorrentHandle &h) const;
bool isTorrentActive(const QTorrentHandle &h) const;
bool isTorrentInactive(const QTorrentHandle &h) const;
bool torrentHasLabel(const QTorrentHandle &h) const;
};
#endif // QTORRENTFILTER_H

14
src/webui/requesthandler.cpp

@ -186,9 +186,21 @@ void RequestHandler::action_public_images() @@ -186,9 +186,21 @@ void RequestHandler::action_public_images()
printFile(path);
}
// GET params:
// - filter (string): all, downloading, completed, paused, active, inactive
// - label (string): torrent label for filtering by it (empty string means "unlabeled"; no "label" param presented means "any label")
// - sort (string): name of column for sorting by its value
// - reverse (bool): enable reverse sorting
// - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited)
// - offset (int): set offset (if less than 0 - offset from end)
void RequestHandler::action_json_torrents()
{
print(btjson::getTorrents(), CONTENT_TYPE_JS);
const QStringMap& gets = request().gets;
print(btjson::getTorrents(
gets["filter"], gets["label"], gets["sort"], gets["reverse"] == "true",
gets["limit"].toInt(), gets["offset"].toInt()
), CONTENT_TYPE_JS);
}
void RequestHandler::action_json_preferences()

6
src/webui/webui.pri

@ -11,7 +11,8 @@ HEADERS += $$PWD/httpserver.h \ @@ -11,7 +11,8 @@ HEADERS += $$PWD/httpserver.h \
$$PWD/extra_translations.h \
$$PWD/webapplication.h \
$$PWD/abstractrequesthandler.h \
$$PWD/requesthandler.h
$$PWD/requesthandler.h \
$$PWD/qtorrentfilter.h
SOURCES += $$PWD/httpserver.cpp \
$$PWD/httpconnection.cpp \
@ -21,7 +22,8 @@ SOURCES += $$PWD/httpserver.cpp \ @@ -21,7 +22,8 @@ SOURCES += $$PWD/httpserver.cpp \
$$PWD/prefjson.cpp \
$$PWD/webapplication.cpp \
$$PWD/abstractrequesthandler.cpp \
$$PWD/requesthandler.cpp
$$PWD/requesthandler.cpp \
$$PWD/qtorrentfilter.cpp
# QJson JSON parser/serializer for using with Qt4
lessThan(QT_MAJOR_VERSION, 5) {

6
src/webui/www/public/filters.html

@ -9,9 +9,9 @@ @@ -9,9 +9,9 @@
<script type="text/javascript">
// Remember this via Cookie
var last_filter = Cookie.read('selected_filter');
var filter = Cookie.read('selected_filter');
if(!$defined(last_filter)) {
last_filter = 'all';
filter = 'all';
}
$(last_filter+'_filter').addClass('selectedFilter');
$(filter+'_filter').addClass('selectedFilter');
</script>

216
src/webui/www/public/scripts/client.js

@ -23,14 +23,11 @@ @@ -23,14 +23,11 @@
*/
myTable = new dynamicTable();
ajaxfn = function(){};
setSortedColumn = function(index){
myTable.setSortedColumn(index);
};
ajaxfn = function () {};
window.addEvent('load', function(){
window.addEvent('load', function () {
var saveColumnSizes = function() {
var saveColumnSizes = function () {
var filters_width = $('Filters').getSize().x;
var properties_height = $('propertiesPanel').getSize().y;
localStorage.setItem('filters_width', filters_width);
@ -45,77 +42,84 @@ window.addEvent('load', function(){ @@ -45,77 +42,84 @@ window.addEvent('load', function(){
MochaUI.Desktop.initialize();
var filt_w = localStorage.getItem('filters_width');
if($defined(filt_w))
if ($defined(filt_w))
filt_w = filt_w.toInt();
else
filt_w = 120;
new MochaUI.Column({
id: 'filtersColumn',
placement: 'left',
onResize: saveColumnSizes,
width: filt_w,
resizeLimit: [100, 300]
id : 'filtersColumn',
placement : 'left',
onResize : saveColumnSizes,
width : filt_w,
resizeLimit : [100, 300]
});
new MochaUI.Column({
id: 'mainColumn',
placement: 'main',
width: null,
resizeLimit: [100, 300]
id : 'mainColumn',
placement : 'main',
width : null,
resizeLimit : [100, 300]
});
MochaUI.Desktop.setDesktopSize();
new MochaUI.Panel({
id: 'Filters',
title: 'Panel',
header: false,
padding: { top: 0, right: 0, bottom: 0, left: 0 },
loadMethod: 'xhr',
contentURL: 'filters.html',
column: 'filtersColumn',
height: 300
id : 'Filters',
title : 'Panel',
header : false,
padding : {
top : 0,
right : 0,
bottom : 0,
left : 0
},
loadMethod : 'xhr',
contentURL : 'filters.html',
column : 'filtersColumn',
height : 300
});
initializeWindows();
var r=0;
var waiting=false;
var r = 0;
var waiting = false;
var waitingTrInfo = false;
var stateToImg = function(state){
if(state == "pausedUP" || state == "pausedDL") {
var stateToImg = function (state) {
if (state == "pausedUP" || state == "pausedDL") {
state = "paused";
} else {
if(state == "queuedUP" || state == "queuedDL") {
if (state == "queuedUP" || state == "queuedDL") {
state = "queued";
} else {
if(state == "checkingUP" || state == "checkingDL") {
if (state == "checkingUP" || state == "checkingDL") {
state = "checking";
}
}
}
return 'images/skin/'+state+'.png';
return 'images/skin/' + state + '.png';
};
var loadTransferInfo = function() {
var loadTransferInfo = function () {
var url = 'json/transferInfo';
if(!waitingTrInfo) {
if (!waitingTrInfo) {
waitingTrInfo = true;
var request = new Request.JSON({
url: url,
noCache: true,
method: 'get',
onFailure: function() {
url : url,
noCache : true,
method : 'get',
onFailure : function () {
$('error_div').set('html', '_(qBittorrent client is not reachable)');
waitingTrInfo=false;
waitingTrInfo = false;
loadTransferInfo.delay(4000);
},
onSuccess: function(info) {
if(info) {
$("DlInfos").set('html', "_(D: %1 - T: %2)".replace("%1", friendlyUnit(info.dl_info_speed, true))
onSuccess : function (info) {
if (info) {
$("DlInfos").set('html', "_(D: %1 - T: %2)"
.replace("%1", friendlyUnit(info.dl_info_speed, true))
.replace("%2", friendlyUnit(info.dl_info_data, false)));
$("UpInfos").set('html', "_(U: %1 - T: %2)".replace("%1", friendlyUnit(info.up_info_speed, true))
$("UpInfos").set('html', "_(U: %1 - T: %2)"
.replace("%1", friendlyUnit(info.up_info_speed, true))
.replace("%2", friendlyUnit(info.up_info_data, false)));
if(localStorage.getItem('speed_in_browser_title_bar') == 'true')
document.title = "_(D:%1 U:%2)".replace("%1", friendlyUnit(info.dl_info_speed, true)).replace("%2", friendlyUnit(info.up_info_speed, true));
else
document.title = "_(qBittorrent web User Interface)";
waitingTrInfo=false;
waitingTrInfo = false;
loadTransferInfo.delay(3000);
}
}
@ -125,27 +129,31 @@ window.addEvent('load', function(){ @@ -125,27 +129,31 @@ window.addEvent('load', function(){
$('DlInfos').addEvent('click', globalDownloadLimitFN);
$('UpInfos').addEvent('click', globalUploadLimitFN);
var ajaxfn = function(){
var ajaxfn = function () {
var queueing_enabled = false;
var url = 'json/torrents';
if (!waiting){
waiting=true;
var url = new URI('json/torrents');
url.setData('filter', filter);
url.setData('sort', myTable.table.sortedColumn);
url.setData('reverse', myTable.table.reverseSort);
if (!waiting) {
waiting = true;
var request = new Request.JSON({
url: url,
noCache: true,
method: 'get',
onFailure: function() {
url : url,
noCache : true,
method : 'get',
onFailure : function () {
$('error_div').set('html', '_(qBittorrent client is not reachable)');
waiting=false;
waiting = false;
ajaxfn.delay(2000);
},
onSuccess: function(events) {
onSuccess : function (events) {
$('error_div').set('html', '');
if(events){
if (events) {
// Add new torrents or update them
torrent_hashes = myTable.getRowIds();
events_hashes = new Array();
events.each(function(event){
pos = 0;
events.each(function (event) {
events_hashes[events_hashes.length] = event.hash;
var row = new Array();
var data = new Array();
@ -156,8 +164,8 @@ window.addEvent('load', function(){ @@ -156,8 +164,8 @@ window.addEvent('load', function(){
data[2] = event.priority;
row[3] = friendlyUnit(event.size, false);
data[3] = event.size;
row[4] = (event.progress*100).round(1);
if(row[4] == 100.0 && event.progress != 1.0)
row[4] = (event.progress * 100).round(1);
if (row[4] == 100.0 && event.progress != 1.0)
row[4] = 99.9;
data[4] = event.progress;
row[5] = event.num_seeds;
@ -173,80 +181,101 @@ window.addEvent('load', function(){ @@ -173,80 +181,101 @@ window.addEvent('load', function(){
row[8] = friendlyUnit(event.upspeed, true);
data[8] = event.upspeed;
row[9] = friendlyDuration(event.eta);
data[9] = event.eta
if(event.ratio == -1)
data[9] = event.eta;
if (event.ratio == -1)
row[10] = "∞";
else
row[10] = (Math.floor(100 * event.ratio) / 100).toFixed(2); //Don't round up
data[10] = event.ratio;
if(row[2] != null)
if (row[2] != null)
queueing_enabled = true;
if(!torrent_hashes.contains(event.hash)) {
if (!torrent_hashes.contains(event.hash)) {
// New unfinished torrent
torrent_hashes[torrent_hashes.length] = event.hash;
//alert("Inserting row");
myTable.insertRow(event.hash, row, data, event.state);
myTable.insertRow(event.hash, row, data, event.state, pos);
} else {
// Update torrent data
myTable.updateRow(event.hash, row, data, event.state);
myTable.updateRow(event.hash, row, data, event.state, pos);
}
pos++;
});
// Remove deleted torrents
torrent_hashes.each(function(hash){
if(!events_hashes.contains(hash)) {
torrent_hashes.each(function (hash) {
if (!events_hashes.contains(hash)) {
myTable.removeRow(hash);
}
});
if(queueing_enabled) {
if (queueing_enabled) {
$('queueingButtons').removeClass('invisible');
myTable.showPriority();
} else {
$('queueingButtons').addClass('invisible');
myTable.hidePriority();
}
myTable.altRow();
}
waiting=false;
waiting = false;
ajaxfn.delay(1500);
}
}).send();
}
};
setSortedColumn = function (column) {
myTable.setSortedColumn(column);
// reload torrents
ajaxfn();
};
new MochaUI.Panel({
id: 'transferList',
title: 'Panel',
header: false,
padding: { top: 0, right: 0, bottom: 0, left: 0 },
loadMethod: 'xhr',
contentURL: 'transferlist.html',
onContentLoaded: function() {
id : 'transferList',
title : 'Panel',
header : false,
padding : {
top : 0,
right : 0,
bottom : 0,
left : 0
},
loadMethod : 'xhr',
contentURL : 'transferlist.html',
onContentLoaded : function () {
ajaxfn();
},
column: 'mainColumn',
onResize: saveColumnSizes,
height: null
column : 'mainColumn',
onResize : saveColumnSizes,
height : null
});
var prop_h = localStorage.getItem('properties_height');
if($defined(prop_h))
if ($defined(prop_h))
prop_h = prop_h.toInt();
else
prop_h = Window.getSize().y / 2.;
new MochaUI.Panel({
id: 'propertiesPanel',
title: 'Panel',
header: true,
padding: { top: 0, right: 0, bottom: 0, left: 0 },
contentURL: 'prop-general.html',
require: {
css: ['css/Tabs.css']
id : 'propertiesPanel',
title : 'Panel',
header : true,
padding : {
top : 0,
right : 0,
bottom : 0,
left : 0
},
contentURL : 'prop-general.html',
require : {
css : ['css/Tabs.css']
},
tabsURL: 'properties.html',
column: 'mainColumn',
height: prop_h
tabsURL : 'properties.html',
column : 'mainColumn',
height : prop_h
});
//ajaxfn();
loadTransferInfo();
setFilter = function(f) {
setFilter = function (f) {
// Visually Select the right filter
$("all_filter").removeClass("selectedFilter");
$("downloading_filter").removeClass("selectedFilter");
@ -254,10 +283,11 @@ window.addEvent('load', function(){ @@ -254,10 +283,11 @@ window.addEvent('load', function(){
$("paused_filter").removeClass("selectedFilter");
$("active_filter").removeClass("selectedFilter");
$("inactive_filter").removeClass("selectedFilter");
$(f+"_filter").addClass("selectedFilter");
myTable.setFilter(f);
ajaxfn();
$(f + "_filter").addClass("selectedFilter");
filter = f;
localStorage.setItem('selected_filter', f);
// Reload torrents
ajaxfn();
}
});
@ -266,7 +296,7 @@ function closeWindows() { @@ -266,7 +296,7 @@ function closeWindows() {
MochaUI.closeAll();
}
window.addEvent('keydown', function(event){
window.addEvent('keydown', function (event) {
if (event.key == 'a' && event.control) {
event.stop();
myTable.selectAll();

258
src/webui/www/public/scripts/dynamicTable.js

@ -29,179 +29,100 @@ @@ -29,179 +29,100 @@
Desc : Programable sortable table
Licence : Open Source MIT Licence
**************************************************************/
**************************************************************/
var dynamicTable = new Class ({
var dynamicTable = new Class({
initialize: function(){
},
initialize : function () {},
setup: function(table, progressIndex, context_menu){
setup : function (table, progressIndex, context_menu) {
this.table = $(table);
this.rows = new Hash();
this.cur = new Array();
this.priority_hidden = false;
this.progressIndex = progressIndex;
this.filter = localStorage.getItem('selected_filter');
if(!$defined(this.filter)) {
this.filter = 'all';
}
this.context_menu = context_menu;
this.table.sortedIndex = 1; // Default is NAME
this.table.sortedColumn = 'name'; // Default is NAME
this.table.reverseSort = false;
},
sortfunction: function(tr1, tr2) {
var i = tr2.getParent().sortedIndex;
var reverseSort = tr2.getParent().reverseSort;
switch(i) {
case 1: // Name
if(!reverseSort)
return tr1.getElements('td')[i].get('html').localeCompare(tr2.getElements('td')[i].get('html'));
else
return tr2.getElements('td')[i].get('html').localeCompare(tr1.getElements('td')[i].get('html'));
case 2: // Prio
case 3: // Size
case 4: // Progress
case 5: // Seeds
case 6: // Peers
case 7: // Up Speed
case 8: // Down Speed
case 9: // ETA
default: // Ratio
if(!reverseSort)
return (tr1.getElements('td')[i].get('data-raw') - tr2.getElements('td')[i].get('data-raw'));
else
return (tr2.getElements('td')[i].get('data-raw') - tr1.getElements('td')[i].get('data-raw'));
}
},
updateSort: function() {
var trs = this.table.getChildren('tr');
trs.sort(this.sortfunction);
this.table.adopt(trs);
},
setSortedColumn: function(index) {
if(index != this.table.sortedIndex) {
this.table.sortedIndex = index;
setSortedColumn : function (column) {
if (column != this.table.sortedColumn) {
this.table.sortedColumn = column;
this.table.reverseSort = false;
} else {
// Toggle sort order
this.table.reverseSort = !this.table.reverseSort;
}
this.updateSort();
this.altRow();
},
getCurrentTorrentHash: function() {
if(this.cur.length > 0)
getCurrentTorrentHash : function () {
if (this.cur.length > 0)
return this.cur[0];
return '';
},
altRow: function()
{
altRow : function () {
var trs = this.table.getElements('tr');
trs.each(function(el,i){
if(i % 2){
trs.each(function (el, i) {
if (i % 2) {
el.addClass('alt');
}else{
} else {
el.removeClass('alt');
}
}.bind(this));
},
hidePriority: function(){
if(this.priority_hidden) return;
hidePriority : function () {
if (this.priority_hidden)
return;
$('prioHeader').addClass('invisible');
var trs = this.table.getElements('tr');
trs.each(function(tr,i){
trs.each(function (tr, i) {
var tds = tr.getElements('td');
tds[2].addClass('invisible');
}.bind(this));
this.priority_hidden = true;
},
setFilter: function(f) {
this.filter = f;
},
showPriority: function(){
if(!this.priority_hidden) return;
showPriority : function () {
if (!this.priority_hidden)
return;
$('prioHeader').removeClass('invisible');
var trs = this.table.getElements('tr');
trs.each(function(tr,i){
trs.each(function (tr, i) {
var tds = tr.getElements('td');
tds[2].removeClass('invisible');
}.bind(this));
this.priority_hidden = false;
},
applyFilterOnRow: function(tr, status) {
switch(this.filter) {
case 'all':
tr.removeClass("invisible");
break;
case 'downloading':
if(status == "downloading" || status == "stalledDL" || status == "checkingDL" || status == "pausedDL" || status == "queuedDL") {
tr.removeClass("invisible");
} else {
tr.addClass("invisible");
}
break;
case 'completed':
if(status == "uploading" || status == "stalledUP" || status == "checkingUP" || status == "pausedUP" || status == "queuedUP") {
tr.removeClass("invisible");
} else {
tr.addClass("invisible");
}
break;
case 'paused':
if(status == "pausedDL" || status == "pausedUP") {
tr.removeClass("invisible");
} else {
tr.addClass("invisible");
}
break;
case 'active':
if(status == "downloading" || status == "uploading") {
tr.removeClass("invisible");
} else {
tr.addClass("invisible");
}
break;
case 'inactive':
if(status != "downloading" && status != "uploading") {
tr.removeClass("invisible");
} else {
tr.addClass("invisible");
}
}
return !tr.hasClass('invisible');
},
insertRow: function(id, row, data, status){
if(this.rows.has(id)) {
insertRow : function (id, row, data, status, pos) {
if (this.rows.has(id)) {
return;
}
var tr = new Element('tr');
tr.addClass("menu-target");
this.rows.set(id, tr);
for(var i=0; i<row.length; i++)
{
for (var i = 0; i < row.length; i++) {
var td = new Element('td');
if(i==this.progressIndex) {
td.adopt(new ProgressBar(row[i].toFloat(), {'id': 'pb_'+id, 'width':80}));
if (i == this.progressIndex) {
td.adopt(new ProgressBar(row[i].toFloat(), {
'id' : 'pb_' + id,
'width' : 80
}));
if (typeof data[i] != 'undefined')
td.set('data-raw', data[i])
} else {
if(i==0) {
td.adopt(new Element('img', {'src':row[i], 'class': 'statusIcon'}));
if (i == 0) {
td.adopt(new Element('img', {
'src' : row[i],
'class' : 'statusIcon'
}));
} else {
if(i==2) {
if (i == 2) {
// Priority
if(this.priority_hidden)
if (this.priority_hidden)
td.addClass('invisible');
}
td.set('html', row[i]);
@ -212,17 +133,17 @@ var dynamicTable = new Class ({ @@ -212,17 +133,17 @@ var dynamicTable = new Class ({
td.injectInside(tr);
};
tr.addEvent('mouseover', function(e){
tr.addEvent('mouseover', function (e) {
tr.addClass('over');
}.bind(this));
tr.addEvent('mouseout', function(e){
tr.addEvent('mouseout', function (e) {
tr.removeClass('over');
}.bind(this));
tr.addEvent('contextmenu', function(e) {
if(!this.cur.contains(id)) {
tr.addEvent('contextmenu', function (e) {
if (!this.cur.contains(id)) {
// Remove selected style from previous ones
for(i=0; i<this.cur.length; i++) {
if(this.rows.has(this.cur[i])) {
for (i = 0; i < this.cur.length; i++) {
if (this.rows.has(this.cur[i])) {
var temptr = this.rows.get(this.cur[i]);
temptr.removeClass('selected');
}
@ -234,28 +155,28 @@ var dynamicTable = new Class ({ @@ -234,28 +155,28 @@ var dynamicTable = new Class ({
}
return true;
}.bind(this));
tr.addEvent('click', function(e){
tr.addEvent('click', function (e) {
e.stop();
if(e.control) {
if (e.control) {
// CTRL key was pressed
if(this.cur.contains(id)) {
if (this.cur.contains(id)) {
// remove it
this.cur.erase(id);
// Remove selected style
if(this.rows.has(id)) {
if (this.rows.has(id)) {
temptr = this.rows.get(id);
temptr.removeClass('selected');
}
} else {
this.cur[this.cur.length] = id;
// Add selected style
if(this.rows.has(id)) {
if (this.rows.has(id)) {
temptr = this.rows.get(id);
temptr.addClass('selected');
}
}
} else {
if(e.shift && this.cur.length == 1) {
if (e.shift && this.cur.length == 1) {
// Shift key was pressed
var first_id = this.cur[0];
var first_tr = this.rows.get(first_id);
@ -264,13 +185,13 @@ var dynamicTable = new Class ({ @@ -264,13 +185,13 @@ var dynamicTable = new Class ({
var all_trs = this.table.getChildren('tr');
var index_first_tr = all_trs.indexOf(first_tr);
var index_last_tr = all_trs.indexOf(last_tr);
var trs_to_select = all_trs.filter(function(item, index){
if(index_first_tr < index_last_tr)
var trs_to_select = all_trs.filter(function (item, index) {
if (index_first_tr < index_last_tr)
return (index > index_first_tr) && (index <= index_last_tr);
else
return (index < index_first_tr) && (index >= index_last_tr);
});
trs_to_select.each(function(item, index){
trs_to_select.each(function (item, index) {
// Add to selection
this.cur[this.cur.length] = this.getRowId(item);
// Select it visually
@ -279,15 +200,15 @@ var dynamicTable = new Class ({ @@ -279,15 +200,15 @@ var dynamicTable = new Class ({
} else {
// Simple selection
// Remove selected style from previous ones
for(i=0; i<this.cur.length; i++) {
if(this.rows.has(this.cur[i])) {
for (i = 0; i < this.cur.length; i++) {
if (this.rows.has(this.cur[i])) {
var temptr = this.rows.get(this.cur[i]);
temptr.removeClass('selected');
}
}
this.cur.empty();
// Add selected style to new one
if(this.rows.has(id)) {
if (this.rows.has(id)) {
temptr = this.rows.get(id);
temptr.addClass('selected');
}
@ -297,49 +218,43 @@ var dynamicTable = new Class ({ @@ -297,49 +218,43 @@ var dynamicTable = new Class ({
}
return false;
}.bind(this));
// Apply filter
this.applyFilterOnRow(tr, status);
// Insert
var trs = this.table.getChildren('tr');
var i=0;
while(i<trs.length && this.sortfunction(tr, trs[i]) > 0) {
i++;
}
if(i==trs.length) {
if (pos >= trs.length) {
tr.inject(this.table);
} else {
tr.inject(trs[i], 'before');
tr.inject(trs[pos], 'before');
}
//tr.injectInside(this.table);
this.altRow();
// Update context menu
this.context_menu.addTarget(tr);
},
selectAll: function() {
selectAll : function () {
this.cur.empty();
this.rows.each(function(tr, id){
this.rows.each(function (tr, id) {
this.cur[this.cur.length] = id;
if(!tr.hasClass('selected')) {
if (!tr.hasClass('selected')) {
tr.addClass('selected');
}
}, this);
},
updateRow: function(id, row, data, status){
if(!this.rows.has(id)) {
updateRow : function (id, row, data, status, newpos) {
if (!this.rows.has(id)) {
return false;
}
var tr = this.rows.get(id);
// Apply filter
if(this.applyFilterOnRow(tr, status)) {
var tds = tr.getElements('td');
for(var i=0; i<row.length; i++) {
if(i==1) continue; // Do not refresh name
if(i==this.progressIndex) {
$('pb_'+id).setValue(row[i]);
for (var i = 0; i < row.length; i++) {
if (i == 1)
continue; // Do not refresh name
if (i == this.progressIndex) {
$('pb_' + id).setValue(row[i]);
} else {
if(i==0) {
if (i == 0) {
tds[i].getChildren('img')[0].set('src', row[i]);
} else {
tds[i].set('html', row[i]);
@ -348,25 +263,26 @@ var dynamicTable = new Class ({ @@ -348,25 +263,26 @@ var dynamicTable = new Class ({
if (typeof data[i] != 'undefined')
tds[i].set('data-raw', data[i])
};
// Prevent freezing of the backlight.
tr.removeClass('over');
// Move to 'newpos'
var trs = this.table.getChildren('tr');
if (newpos >= trs.length) {
tr.inject(this.table);
} else {
// Row was hidden, check if it was selected
// and unselect it if it was
if(this.cur.contains(id)) {
// Remove from selection
this.cur.erase(id);
// Remove selected style
tr.removeClass('selected');
}
tr.inject(trs[newpos], 'before');
}
return true;
},
removeRow: function(id){
if(this.cur.contains(id))
{
removeRow : function (id) {
if (this.cur.contains(id)) {
this.cur.erase(id);
}
if(this.rows.has(id)) {
if (this.rows.has(id)) {
var tr = this.rows.get(id);
tr.dispose();
this.altRow();
@ -376,19 +292,19 @@ var dynamicTable = new Class ({ @@ -376,19 +292,19 @@ var dynamicTable = new Class ({
return false;
},
selectedIds: function(){
selectedIds : function () {
return this.cur;
},
getRowId: function(tr){
getRowId : function (tr) {
return this.rows.keyOf(tr);
},
getRowIds: function(){
getRowIds : function () {
return this.rows.getKeys();
}
});
});
//dynamicTable.implement(new Options);
//dynamicTable.implement(new Events);

54
src/webui/www/public/transferlist.html

@ -2,16 +2,16 @@ @@ -2,16 +2,16 @@
<thead>
<tr>
<th style="width: 0"></th>
<th onClick="setSortedColumn(1);" style="cursor: pointer;">_(Name)</th>
<th id='prioHeader' onClick="setSortedColumn(2);" style="cursor: pointer;">#</th>
<th onClick="setSortedColumn(3);" style="cursor: pointer;">_(Size)</th>
<th style="width: 90px;cursor: pointer;" onClick="setSortedColumn(4);">_(Done)</th>
<th onClick="setSortedColumn(5);" style="cursor: pointer;">_(Seeds)</th>
<th onClick="setSortedColumn(6);" style="cursor: pointer;">_(Peers)</th>
<th onClick="setSortedColumn(7);" style="cursor: pointer;">_(Down Speed)</th>
<th onClick="setSortedColumn(8);" style="cursor: pointer;">_(Up Speed)</th>
<th onClick="setSortedColumn(9);" style="cursor: pointer;">_(ETA)</th>
<th onClick="setSortedColumn(10);" style="cursor: pointer;">_(Ratio)</th>
<th onClick="setSortedColumn('name');" style="cursor: pointer;">_(Name)</th>
<th id='prioHeader' onClick="setSortedColumn('priority');" style="cursor: pointer;">#</th>
<th onClick="setSortedColumn('size');" style="cursor: pointer;">_(Size)</th>
<th style="width: 90px;cursor: pointer;" onClick="setSortedColumn('progress');">_(Done)</th>
<th onClick="setSortedColumn('num_seeds');" style="cursor: pointer;">_(Seeds)</th>
<th onClick="setSortedColumn('num_leechs');" style="cursor: pointer;">_(Peers)</th>
<th onClick="setSortedColumn('dlspeed');" style="cursor: pointer;">_(Down Speed)</th>
<th onClick="setSortedColumn('upspeed');" style="cursor: pointer;">_(Up Speed)</th>
<th onClick="setSortedColumn('eta');" style="cursor: pointer;">_(ETA)</th>
<th onClick="setSortedColumn('ratio');" style="cursor: pointer;">_(Ratio)</th>
</tr>
</thead>
@ -22,46 +22,48 @@ @@ -22,46 +22,48 @@
//create a context menu
var context_menu = new ContextMenu({
targets: '.menu-target',
menu: 'contextmenu',
actions: {
Delete: function(element,ref) {
targets : '.menu-target',
menu : 'contextmenu',
actions : {
Delete : function (element, ref) {
deleteFN();
},
DeleteHD: function(element,ref) {
DeleteHD : function (element, ref) {
deleteHDFN();
},
Start: function(element, ref) {
Start : function (element, ref) {
startFN();
},
Pause: function(element, ref) {
Pause : function (element, ref) {
pauseFN();
},
prioTop: function(element, ref) {
prioTop : function (element, ref) {
setPriorityFN('topPrio');
},
prioUp: function(element, ref) {
prioUp : function (element, ref) {
setPriorityFN('increasePrio');
},
prioDown: function(element, ref) {
prioDown : function (element, ref) {
setPriorityFN('decreasePrio');
},
prioBottom: function(element, ref) {
prioBottom : function (element, ref) {
setPriorityFN('bottomPrio');
},
ForceRecheck: function(element, ref) {
ForceRecheck : function (element, ref) {
recheckFN();
},
UploadLimit: function(element, red) {
UploadLimit : function (element, red) {
uploadLimitFN();
},
DownloadLimit: function(element, red) {
DownloadLimit : function (element, red) {
downloadLimitFN();
}
},
offsets: { x:-15, y:2 }
offsets : {
x : -15,
y : 2
}
});
myTable.setup('myTable', 4, context_menu);
</script>

Loading…
Cancel
Save