feat: web client refresh (#1476)
Give the web client a major overhaul. User-visible highlights include: * Mobile is now fully supported. * Added fullscreen support on mobile. * Better support for dark mode. * Added mime icons to the torrent list. * Improved theme consistency across the app. Maintainer highlights include: * Updated code to use ES6 APIs. * No longer uses jQuery UI. * No longer uses jQuery. * Use Webpack to bundle the Javascript, CSS, and assets together -- the entire bundle size is now 68K gzipped. * Added eslint / prettier / stylelint tooling. * Uses torrent-get's 'table' mode for more efficient RPC calls.
|
@ -19,3 +19,4 @@ macosx/en.lproj/*~.nib
|
||||||
node_modules/
|
node_modules/
|
||||||
po/*.mo
|
po/*.mo
|
||||||
third-party/miniupnp/miniupnpcstrings.h
|
third-party/miniupnp/miniupnpcstrings.h
|
||||||
|
web/public_html/transmission-app.js.map
|
||||||
|
|
|
@ -17,6 +17,7 @@ include(TrMacros)
|
||||||
tr_auto_option(ENABLE_GTK "Build GTK+ client" AUTO)
|
tr_auto_option(ENABLE_GTK "Build GTK+ client" AUTO)
|
||||||
tr_auto_option(ENABLE_QT "Build Qt client" AUTO)
|
tr_auto_option(ENABLE_QT "Build Qt client" AUTO)
|
||||||
tr_auto_option(ENABLE_MAC "Build Mac client" AUTO)
|
tr_auto_option(ENABLE_MAC "Build Mac client" AUTO)
|
||||||
|
option(ENABLE_WEB "Build Web client" OFF)
|
||||||
option(ENABLE_UTILS "Build utils (create, edit, show)" ON)
|
option(ENABLE_UTILS "Build utils (create, edit, show)" ON)
|
||||||
option(ENABLE_CLI "Build command-line client" OFF)
|
option(ENABLE_CLI "Build command-line client" OFF)
|
||||||
option(ENABLE_TESTS "Build unit tests" ON)
|
option(ENABLE_TESTS "Build unit tests" ON)
|
||||||
|
@ -622,10 +623,7 @@ if(ENABLE_TESTS)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
function(tr_install_web DST_DIR)
|
function(tr_install_web DST_DIR)
|
||||||
install(DIRECTORY ${CMAKE_SOURCE_DIR}/web DESTINATION ${DST_DIR}
|
install(DIRECTORY ${CMAKE_SOURCE_DIR}/web/public_html DESTINATION ${DST_DIR})
|
||||||
PATTERN *.am EXCLUDE
|
|
||||||
PATTERN *.in EXCLUDE
|
|
||||||
PATTERN *.scss EXCLUDE)
|
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
add_subdirectory(libtransmission)
|
add_subdirectory(libtransmission)
|
||||||
|
@ -637,7 +635,7 @@ if(ENABLE_GTK AND ENABLE_NLS)
|
||||||
add_subdirectory(po)
|
add_subdirectory(po)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
foreach(P daemon cli utils gtk qt mac)
|
foreach(P cli daemon gtk mac qt utils web)
|
||||||
string(TOUPPER "${P}" P_ID)
|
string(TOUPPER "${P}" P_ID)
|
||||||
if(ENABLE_${P_ID})
|
if(ENABLE_${P_ID})
|
||||||
if(DEFINED ${P_ID}_PROJECT_DIR)
|
if(DEFINED ${P_ID}_PROJECT_DIR)
|
||||||
|
@ -664,7 +662,11 @@ set(CPACK_SOURCE_PACKAGE_FILE_NAME "${TR_NAME}-${TR_USER_AGENT_PREFIX}")
|
||||||
if(NOT TR_STABLE_RELEASE)
|
if(NOT TR_STABLE_RELEASE)
|
||||||
string(APPEND CPACK_SOURCE_PACKAGE_FILE_NAME "-r${TR_VCS_REVISION}")
|
string(APPEND CPACK_SOURCE_PACKAGE_FILE_NAME "-r${TR_VCS_REVISION}")
|
||||||
endif()
|
endif()
|
||||||
list(APPEND CPACK_SOURCE_IGNORE_FILES [.]git)
|
list(APPEND CPACK_SOURCE_IGNORE_FILES
|
||||||
|
"${CMAKE_BINARY_DIR}"
|
||||||
|
"[.]git"
|
||||||
|
"node_modules"
|
||||||
|
)
|
||||||
set(CPACK_INSTALL_SCRIPTS "${CMAKE_CURRENT_LIST_DIR}/cmake/CPackSourceFixDirLinks.cmake")
|
set(CPACK_INSTALL_SCRIPTS "${CMAKE_CURRENT_LIST_DIR}/cmake/CPackSourceFixDirLinks.cmake")
|
||||||
|
|
||||||
## Code Formatting
|
## Code Formatting
|
||||||
|
|
|
@ -62,7 +62,10 @@ fi
|
||||||
cd "${root}/web" || exit 1
|
cd "${root}/web" || exit 1
|
||||||
if [ -n "$fix" ]; then
|
if [ -n "$fix" ]; then
|
||||||
cd "${root}/web" && yarn --silent install && yarn --silent 'lint:fix'
|
cd "${root}/web" && yarn --silent install && yarn --silent 'lint:fix'
|
||||||
elif ! yarn -s install && yarn --silent lint; then
|
elif ! yarn -s install; then
|
||||||
|
echo 'JS code could not be checked -- "yarn install" failed'
|
||||||
|
exitcode=1
|
||||||
|
elif ! yarn --silent lint; then
|
||||||
echo 'JS code needs formatting'
|
echo 'JS code needs formatting'
|
||||||
exitcode=1
|
exitcode=1
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -53,7 +53,7 @@ png2ico(Transmission.ico
|
||||||
"${ICONS_DIR}/192x192/transmission-qt.png"
|
"${ICONS_DIR}/192x192/transmission-qt.png"
|
||||||
"${ICONS_DIR}/256x256/transmission-qt.png")
|
"${ICONS_DIR}/256x256/transmission-qt.png")
|
||||||
|
|
||||||
set(WEBSRCDIR "${CMAKE_INSTALL_PREFIX}/share/transmission/web")
|
set(WEBSRCDIR "${CMAKE_INSTALL_PREFIX}/share/transmission/public_html")
|
||||||
set(TRQMSRCDIR "${CMAKE_INSTALL_PREFIX}/share/transmission/translations")
|
set(TRQMSRCDIR "${CMAKE_INSTALL_PREFIX}/share/transmission/translations")
|
||||||
set(QTQMSRCDIR "${TR_QT_DIR}/translations")
|
set(QTQMSRCDIR "${TR_QT_DIR}/translations")
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
<Merge Id="VCRedist" SourceFile="$(var.MsvcCrtMsmFile)" DiskId="1" Language="0" />
|
<Merge Id="VCRedist" SourceFile="$(var.MsvcCrtMsmFile)" DiskId="1" Language="0" />
|
||||||
<Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
|
<Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
|
||||||
<Directory Id="INSTALLDIR" Name="Transmission">
|
<Directory Id="INSTALLDIR" Name="Transmission">
|
||||||
<Directory Id="WEBINSTALLDIR" Name="web" />
|
<Directory Id="WEBINSTALLDIR" Name="public_html" />
|
||||||
</Directory>
|
</Directory>
|
||||||
</Directory>
|
</Directory>
|
||||||
<Directory Id="ProgramMenuFolder" Name="Programs"/>
|
<Directory Id="ProgramMenuFolder" Name="Programs"/>
|
||||||
|
|
|
@ -493,7 +493,7 @@ char const* tr_getWebClientDir(tr_session const* session)
|
||||||
#ifdef BUILD_MAC_CLIENT /* on Mac, look in the Application Support folder first, then in the app bundle. */
|
#ifdef BUILD_MAC_CLIENT /* on Mac, look in the Application Support folder first, then in the app bundle. */
|
||||||
|
|
||||||
/* Look in the Application Support folder */
|
/* Look in the Application Support folder */
|
||||||
s = tr_buildPath(tr_sessionGetConfigDir(session), "web", NULL);
|
s = tr_buildPath(tr_sessionGetConfigDir(session), "public_html", NULL);
|
||||||
|
|
||||||
if (!isWebClientDir(s))
|
if (!isWebClientDir(s))
|
||||||
{
|
{
|
||||||
|
@ -511,7 +511,7 @@ char const* tr_getWebClientDir(tr_session const* session)
|
||||||
CFRelease(appRef);
|
CFRelease(appRef);
|
||||||
|
|
||||||
/* Fallback to the app bundle */
|
/* Fallback to the app bundle */
|
||||||
s = tr_buildPath(appString, "Contents", "Resources", "web", NULL);
|
s = tr_buildPath(appString, "Contents", "Resources", "public_html", NULL);
|
||||||
|
|
||||||
if (!isWebClientDir(s))
|
if (!isWebClientDir(s))
|
||||||
{
|
{
|
||||||
|
@ -628,7 +628,7 @@ char const* tr_getWebClientDir(tr_session const* session)
|
||||||
/* walk through the candidates & look for a match */
|
/* walk through the candidates & look for a match */
|
||||||
for (tr_list* l = candidates; l != NULL; l = l->next)
|
for (tr_list* l = candidates; l != NULL; l = l->next)
|
||||||
{
|
{
|
||||||
char* path = tr_buildPath(l->data, "transmission", "web", NULL);
|
char* path = tr_buildPath(l->data, "transmission", "public_html", NULL);
|
||||||
bool const found = isWebClientDir(path);
|
bool const found = isWebClientDir(path);
|
||||||
|
|
||||||
if (found)
|
if (found)
|
||||||
|
|
|
@ -279,7 +279,8 @@ static char const* mimetype_guess(char const* path)
|
||||||
{ "html", "text/html" },
|
{ "html", "text/html" },
|
||||||
{ "ico", "image/vnd.microsoft.icon" },
|
{ "ico", "image/vnd.microsoft.icon" },
|
||||||
{ "js", "application/javascript" },
|
{ "js", "application/javascript" },
|
||||||
{ "png", "image/png" }
|
{ "png", "image/png" },
|
||||||
|
{ "svg", "image/svg+xml" }
|
||||||
};
|
};
|
||||||
char const* dot = strrchr(path, '.');
|
char const* dot = strrchr(path, '.');
|
||||||
|
|
||||||
|
@ -1264,5 +1265,11 @@ tr_rpc_server* tr_rpcInit(tr_session* session, tr_variant* settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char const* webClientDir = tr_getWebClientDir(s->session);
|
||||||
|
if (!tr_str_is_empty(webClientDir))
|
||||||
|
{
|
||||||
|
tr_logAddNamedInfo(MY_NAME, _("Serving RPC and Web requests from directory '%s'"), webClientDir);
|
||||||
|
}
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
module.exports = {
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"commonjs": true,
|
||||||
|
"es6": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:sonarjs/recommended",
|
||||||
|
"plugin:unicorn/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@babel/eslint-parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"sonarjs",
|
||||||
|
"unicorn"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"accessor-pairs": "error",
|
||||||
|
"array-callback-return": "error",
|
||||||
|
"arrow-spacing": "error",
|
||||||
|
"block-scoped-var": "error",
|
||||||
|
"class-methods-use-this": "error",
|
||||||
|
"consistent-return": "error",
|
||||||
|
"curly": "error",
|
||||||
|
"default-case": "error",
|
||||||
|
"default-case-last": "error",
|
||||||
|
"default-param-last": "error",
|
||||||
|
"eqeqeq": "error",
|
||||||
|
"grouped-accessor-pairs": "error",
|
||||||
|
"guard-for-in": "error",
|
||||||
|
"init-declarations": "error",
|
||||||
|
"no-array-constructor": "error",
|
||||||
|
"no-caller": "error",
|
||||||
|
"no-confusing-arrow": "error",
|
||||||
|
"no-constructor-return": "error",
|
||||||
|
"no-delete-var": "error",
|
||||||
|
"no-dupe-class-members": "error",
|
||||||
|
"no-duplicate-imports": "error",
|
||||||
|
"no-else-return": "error",
|
||||||
|
"no-empty-function": "error",
|
||||||
|
"no-eq-null": "error",
|
||||||
|
"no-eval": "error",
|
||||||
|
"no-extend-native": "error",
|
||||||
|
"no-extra-bind": "error",
|
||||||
|
"no-extra-label": "error",
|
||||||
|
"no-floating-decimal": "error",
|
||||||
|
"no-implicit-coercion": "error",
|
||||||
|
"no-implicit-globals": "error",
|
||||||
|
"no-implied-eval": "error",
|
||||||
|
"no-invalid-this": "error",
|
||||||
|
"no-iterator": "error",
|
||||||
|
"no-label-var": "error",
|
||||||
|
"no-labels": "error",
|
||||||
|
"no-lone-blocks": "error",
|
||||||
|
"no-loop-func": "error",
|
||||||
|
"no-loss-of-precision": "error",
|
||||||
|
"no-multi-str": "error",
|
||||||
|
"no-nested-ternary": "error",
|
||||||
|
"no-new": "error",
|
||||||
|
"no-new-func": "error",
|
||||||
|
"no-new-wrappers": "error",
|
||||||
|
"no-octal": "error",
|
||||||
|
"no-octal-escape": "error",
|
||||||
|
"no-promise-executor-return": "error",
|
||||||
|
"no-proto": "error",
|
||||||
|
"no-redeclare": "error",
|
||||||
|
"no-restricted-exports": "error",
|
||||||
|
"no-restricted-globals": "error",
|
||||||
|
"no-restricted-imports": "error",
|
||||||
|
"no-restricted-properties": "error",
|
||||||
|
"no-return-assign": "error",
|
||||||
|
"no-script-url": "error",
|
||||||
|
"no-self-compare": "error",
|
||||||
|
"no-sequences": "error",
|
||||||
|
"no-shadow": "error",
|
||||||
|
"no-template-curly-in-string": "error",
|
||||||
|
"no-this-before-super": "error",
|
||||||
|
"no-throw-literal": "error",
|
||||||
|
"no-undef-init": "error",
|
||||||
|
"no-undefined": "error",
|
||||||
|
"no-unmodified-loop-condition": "error",
|
||||||
|
"no-unreachable-loop": "error",
|
||||||
|
"no-unused-expressions": "error",
|
||||||
|
"no-unused-labels": "error",
|
||||||
|
"no-unused-vars": "error",
|
||||||
|
"no-use-before-define": "error",
|
||||||
|
"no-useless-backreference": "error",
|
||||||
|
"no-useless-call": "error",
|
||||||
|
"no-useless-catch": "error",
|
||||||
|
"no-useless-computed-key": "error",
|
||||||
|
"no-useless-concat": "error",
|
||||||
|
"no-useless-constructor": "error",
|
||||||
|
"no-useless-escape": "error",
|
||||||
|
"no-useless-rename": "error",
|
||||||
|
"no-useless-return": "error",
|
||||||
|
"no-var": "error",
|
||||||
|
"no-void": "error",
|
||||||
|
"no-with": "error",
|
||||||
|
"object-shorthand": "error",
|
||||||
|
"prefer-arrow-callback": "error",
|
||||||
|
"prefer-const": "error",
|
||||||
|
"prefer-destructuring": "error",
|
||||||
|
"prefer-exponentiation-operator": "error",
|
||||||
|
"prefer-numeric-literals": "error",
|
||||||
|
"prefer-object-spread": "error",
|
||||||
|
"prefer-promise-reject-errors": "error",
|
||||||
|
"prefer-regex-literals": "error",
|
||||||
|
"prefer-rest-params": "error",
|
||||||
|
"prefer-spread": "error",
|
||||||
|
"prefer-template": "error",
|
||||||
|
"radix": "error",
|
||||||
|
"require-atomic-updates": "error",
|
||||||
|
"require-await": "error",
|
||||||
|
"semi": "error",
|
||||||
|
"sonarjs/cognitive-complexity": "off",
|
||||||
|
"sonarjs/no-duplicate-string": "off",
|
||||||
|
"sort-keys": "error",
|
||||||
|
"strict": "error",
|
||||||
|
'unicorn/consistent-function-scoping': 'off',
|
||||||
|
"unicorn/no-fn-reference-in-iterator": "off",
|
||||||
|
"unicorn/no-null": "off",
|
||||||
|
"unicorn/no-reduce": "off",
|
||||||
|
"unicorn/no-unused-properties": "off",
|
||||||
|
"unicorn/prevent-abbreviations": "off",
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,78 @@
|
||||||
|
project(trweb)
|
||||||
|
|
||||||
|
set(TRWEB_SRCS
|
||||||
|
src/about-dialog.js
|
||||||
|
src/action-manager.js
|
||||||
|
src/alert-dialog.js
|
||||||
|
src/context-menu.js
|
||||||
|
src/file-row.js
|
||||||
|
src/formatter.js
|
||||||
|
src/inspector.js
|
||||||
|
src/main.js
|
||||||
|
src/move-dialog.js
|
||||||
|
src/notifications.js
|
||||||
|
src/open-dialog.js
|
||||||
|
src/overflow-menu.js
|
||||||
|
src/prefs-dialog.js
|
||||||
|
src/prefs.js
|
||||||
|
src/remote.js
|
||||||
|
src/remove-dialog.js
|
||||||
|
src/rename-dialog.js
|
||||||
|
src/shortcuts-dialog.js
|
||||||
|
src/statistics-dialog.js
|
||||||
|
src/torrent.js
|
||||||
|
src/torrent-row.js
|
||||||
|
src/transmission.js
|
||||||
|
src/utils.js
|
||||||
|
style/transmission-app.scss
|
||||||
|
)
|
||||||
|
|
||||||
|
set(TRWEB_IMGS
|
||||||
|
style/images/analytics.svg
|
||||||
|
style/images/application-x-executable.png
|
||||||
|
style/images/audio-x-generic.png
|
||||||
|
style/images/blue-turtle.png
|
||||||
|
style/images/checkered-flag.svg
|
||||||
|
style/images/chevron-down.svg
|
||||||
|
style/images/chevron-up.svg
|
||||||
|
style/images/diagram-3-fill.svg
|
||||||
|
style/images/files.svg
|
||||||
|
style/images/folder.png
|
||||||
|
style/images/font-x-generic.png
|
||||||
|
style/images/gear-fill.svg
|
||||||
|
style/images/horizontal-rule.svg
|
||||||
|
style/images/image-x-generic.png
|
||||||
|
style/images/lock-fill.svg
|
||||||
|
style/images/logo.png
|
||||||
|
style/images/package-x-generic.png
|
||||||
|
style/images/pause-circle-active.svg
|
||||||
|
style/images/pause-circle-idle.svg
|
||||||
|
style/images/play-circle-active.svg
|
||||||
|
style/images/play-circle-idle.svg
|
||||||
|
style/images/router.svg
|
||||||
|
style/images/search.svg
|
||||||
|
style/images/team.svg
|
||||||
|
style/images/text-x-generic.png
|
||||||
|
style/images/three-dots-vertical.svg
|
||||||
|
style/images/toolbar-close.png
|
||||||
|
style/images/toolbar-folder.png
|
||||||
|
style/images/toolbar-info.png
|
||||||
|
style/images/toolbar-pause.png
|
||||||
|
style/images/toolbar-start.png
|
||||||
|
style/images/turtle.png
|
||||||
|
style/images/up-and-down-arrows.svg
|
||||||
|
style/images/video-x-generic.png
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(
|
||||||
|
trweb ALL
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/package.json" "${CMAKE_CURRENT_BINARY_DIR}/package.json"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/yarn.lock" "${CMAKE_CURRENT_BINARY_DIR}/yarn.lock"
|
||||||
|
COMMAND yarn install
|
||||||
|
COMMAND yarn webpack --config "${CMAKE_CURRENT_SOURCE_DIR}/webpack.config.js" --context "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
|
BYPRODUCTS
|
||||||
|
public_html/transmission-app.js
|
||||||
|
public_html/transmission-app.js.LICENSE.txt
|
||||||
|
DEPENDS ${TRWEB_IMGS}
|
||||||
|
SOURCES ${TRWEB_SRCS}
|
||||||
|
)
|
339
web/LICENSE
|
@ -1,339 +0,0 @@
|
||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 2, June 1991
|
|
||||||
|
|
||||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The licenses for most software are designed to take away your
|
|
||||||
freedom to share and change it. By contrast, the GNU General Public
|
|
||||||
License is intended to guarantee your freedom to share and change free
|
|
||||||
software--to make sure the software is free for all its users. This
|
|
||||||
General Public License applies to most of the Free Software
|
|
||||||
Foundation's software and to any other program whose authors commit to
|
|
||||||
using it. (Some other Free Software Foundation software is covered by
|
|
||||||
the GNU Lesser General Public License instead.) You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
this service if you wish), that you receive source code or can get it
|
|
||||||
if you want it, that you can change the software or use pieces of it
|
|
||||||
in new free programs; and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to make restrictions that forbid
|
|
||||||
anyone to deny you these rights or to ask you to surrender the rights.
|
|
||||||
These restrictions translate to certain responsibilities for you if you
|
|
||||||
distribute copies of the software, or if you modify it.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must give the recipients all the rights that
|
|
||||||
you have. You must make sure that they, too, receive or can get the
|
|
||||||
source code. And you must show them these terms so they know their
|
|
||||||
rights.
|
|
||||||
|
|
||||||
We protect your rights with two steps: (1) copyright the software, and
|
|
||||||
(2) offer you this license which gives you legal permission to copy,
|
|
||||||
distribute and/or modify the software.
|
|
||||||
|
|
||||||
Also, for each author's protection and ours, we want to make certain
|
|
||||||
that everyone understands that there is no warranty for this free
|
|
||||||
software. If the software is modified by someone else and passed on, we
|
|
||||||
want its recipients to know that what they have is not the original, so
|
|
||||||
that any problems introduced by others will not reflect on the original
|
|
||||||
authors' reputations.
|
|
||||||
|
|
||||||
Finally, any free program is threatened constantly by software
|
|
||||||
patents. We wish to avoid the danger that redistributors of a free
|
|
||||||
program will individually obtain patent licenses, in effect making the
|
|
||||||
program proprietary. To prevent this, we have made it clear that any
|
|
||||||
patent must be licensed for everyone's free use or not licensed at all.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
||||||
|
|
||||||
0. This License applies to any program or other work which contains
|
|
||||||
a notice placed by the copyright holder saying it may be distributed
|
|
||||||
under the terms of this General Public License. The "Program", below,
|
|
||||||
refers to any such program or work, and a "work based on the Program"
|
|
||||||
means either the Program or any derivative work under copyright law:
|
|
||||||
that is to say, a work containing the Program or a portion of it,
|
|
||||||
either verbatim or with modifications and/or translated into another
|
|
||||||
language. (Hereinafter, translation is included without limitation in
|
|
||||||
the term "modification".) Each licensee is addressed as "you".
|
|
||||||
|
|
||||||
Activities other than copying, distribution and modification are not
|
|
||||||
covered by this License; they are outside its scope. The act of
|
|
||||||
running the Program is not restricted, and the output from the Program
|
|
||||||
is covered only if its contents constitute a work based on the
|
|
||||||
Program (independent of having been made by running the Program).
|
|
||||||
Whether that is true depends on what the Program does.
|
|
||||||
|
|
||||||
1. You may copy and distribute verbatim copies of the Program's
|
|
||||||
source code as you receive it, in any medium, provided that you
|
|
||||||
conspicuously and appropriately publish on each copy an appropriate
|
|
||||||
copyright notice and disclaimer of warranty; keep intact all the
|
|
||||||
notices that refer to this License and to the absence of any warranty;
|
|
||||||
and give any other recipients of the Program a copy of this License
|
|
||||||
along with the Program.
|
|
||||||
|
|
||||||
You may charge a fee for the physical act of transferring a copy, and
|
|
||||||
you may at your option offer warranty protection in exchange for a fee.
|
|
||||||
|
|
||||||
2. You may modify your copy or copies of the Program or any portion
|
|
||||||
of it, thus forming a work based on the Program, and copy and
|
|
||||||
distribute such modifications or work under the terms of Section 1
|
|
||||||
above, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) You must cause the modified files to carry prominent notices
|
|
||||||
stating that you changed the files and the date of any change.
|
|
||||||
|
|
||||||
b) You must cause any work that you distribute or publish, that in
|
|
||||||
whole or in part contains or is derived from the Program or any
|
|
||||||
part thereof, to be licensed as a whole at no charge to all third
|
|
||||||
parties under the terms of this License.
|
|
||||||
|
|
||||||
c) If the modified program normally reads commands interactively
|
|
||||||
when run, you must cause it, when started running for such
|
|
||||||
interactive use in the most ordinary way, to print or display an
|
|
||||||
announcement including an appropriate copyright notice and a
|
|
||||||
notice that there is no warranty (or else, saying that you provide
|
|
||||||
a warranty) and that users may redistribute the program under
|
|
||||||
these conditions, and telling the user how to view a copy of this
|
|
||||||
License. (Exception: if the Program itself is interactive but
|
|
||||||
does not normally print such an announcement, your work based on
|
|
||||||
the Program is not required to print an announcement.)
|
|
||||||
|
|
||||||
These requirements apply to the modified work as a whole. If
|
|
||||||
identifiable sections of that work are not derived from the Program,
|
|
||||||
and can be reasonably considered independent and separate works in
|
|
||||||
themselves, then this License, and its terms, do not apply to those
|
|
||||||
sections when you distribute them as separate works. But when you
|
|
||||||
distribute the same sections as part of a whole which is a work based
|
|
||||||
on the Program, the distribution of the whole must be on the terms of
|
|
||||||
this License, whose permissions for other licensees extend to the
|
|
||||||
entire whole, and thus to each and every part regardless of who wrote it.
|
|
||||||
|
|
||||||
Thus, it is not the intent of this section to claim rights or contest
|
|
||||||
your rights to work written entirely by you; rather, the intent is to
|
|
||||||
exercise the right to control the distribution of derivative or
|
|
||||||
collective works based on the Program.
|
|
||||||
|
|
||||||
In addition, mere aggregation of another work not based on the Program
|
|
||||||
with the Program (or with a work based on the Program) on a volume of
|
|
||||||
a storage or distribution medium does not bring the other work under
|
|
||||||
the scope of this License.
|
|
||||||
|
|
||||||
3. You may copy and distribute the Program (or a work based on it,
|
|
||||||
under Section 2) in object code or executable form under the terms of
|
|
||||||
Sections 1 and 2 above provided that you also do one of the following:
|
|
||||||
|
|
||||||
a) Accompany it with the complete corresponding machine-readable
|
|
||||||
source code, which must be distributed under the terms of Sections
|
|
||||||
1 and 2 above on a medium customarily used for software interchange; or,
|
|
||||||
|
|
||||||
b) Accompany it with a written offer, valid for at least three
|
|
||||||
years, to give any third party, for a charge no more than your
|
|
||||||
cost of physically performing source distribution, a complete
|
|
||||||
machine-readable copy of the corresponding source code, to be
|
|
||||||
distributed under the terms of Sections 1 and 2 above on a medium
|
|
||||||
customarily used for software interchange; or,
|
|
||||||
|
|
||||||
c) Accompany it with the information you received as to the offer
|
|
||||||
to distribute corresponding source code. (This alternative is
|
|
||||||
allowed only for noncommercial distribution and only if you
|
|
||||||
received the program in object code or executable form with such
|
|
||||||
an offer, in accord with Subsection b above.)
|
|
||||||
|
|
||||||
The source code for a work means the preferred form of the work for
|
|
||||||
making modifications to it. For an executable work, complete source
|
|
||||||
code means all the source code for all modules it contains, plus any
|
|
||||||
associated interface definition files, plus the scripts used to
|
|
||||||
control compilation and installation of the executable. However, as a
|
|
||||||
special exception, the source code distributed need not include
|
|
||||||
anything that is normally distributed (in either source or binary
|
|
||||||
form) with the major components (compiler, kernel, and so on) of the
|
|
||||||
operating system on which the executable runs, unless that component
|
|
||||||
itself accompanies the executable.
|
|
||||||
|
|
||||||
If distribution of executable or object code is made by offering
|
|
||||||
access to copy from a designated place, then offering equivalent
|
|
||||||
access to copy the source code from the same place counts as
|
|
||||||
distribution of the source code, even though third parties are not
|
|
||||||
compelled to copy the source along with the object code.
|
|
||||||
|
|
||||||
4. You may not copy, modify, sublicense, or distribute the Program
|
|
||||||
except as expressly provided under this License. Any attempt
|
|
||||||
otherwise to copy, modify, sublicense or distribute the Program is
|
|
||||||
void, and will automatically terminate your rights under this License.
|
|
||||||
However, parties who have received copies, or rights, from you under
|
|
||||||
this License will not have their licenses terminated so long as such
|
|
||||||
parties remain in full compliance.
|
|
||||||
|
|
||||||
5. You are not required to accept this License, since you have not
|
|
||||||
signed it. However, nothing else grants you permission to modify or
|
|
||||||
distribute the Program or its derivative works. These actions are
|
|
||||||
prohibited by law if you do not accept this License. Therefore, by
|
|
||||||
modifying or distributing the Program (or any work based on the
|
|
||||||
Program), you indicate your acceptance of this License to do so, and
|
|
||||||
all its terms and conditions for copying, distributing or modifying
|
|
||||||
the Program or works based on it.
|
|
||||||
|
|
||||||
6. Each time you redistribute the Program (or any work based on the
|
|
||||||
Program), the recipient automatically receives a license from the
|
|
||||||
original licensor to copy, distribute or modify the Program subject to
|
|
||||||
these terms and conditions. You may not impose any further
|
|
||||||
restrictions on the recipients' exercise of the rights granted herein.
|
|
||||||
You are not responsible for enforcing compliance by third parties to
|
|
||||||
this License.
|
|
||||||
|
|
||||||
7. If, as a consequence of a court judgment or allegation of patent
|
|
||||||
infringement or for any other reason (not limited to patent issues),
|
|
||||||
conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot
|
|
||||||
distribute so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you
|
|
||||||
may not distribute the Program at all. For example, if a patent
|
|
||||||
license would not permit royalty-free redistribution of the Program by
|
|
||||||
all those who receive copies directly or indirectly through you, then
|
|
||||||
the only way you could satisfy both it and this License would be to
|
|
||||||
refrain entirely from distribution of the Program.
|
|
||||||
|
|
||||||
If any portion of this section is held invalid or unenforceable under
|
|
||||||
any particular circumstance, the balance of the section is intended to
|
|
||||||
apply and the section as a whole is intended to apply in other
|
|
||||||
circumstances.
|
|
||||||
|
|
||||||
It is not the purpose of this section to induce you to infringe any
|
|
||||||
patents or other property right claims or to contest validity of any
|
|
||||||
such claims; this section has the sole purpose of protecting the
|
|
||||||
integrity of the free software distribution system, which is
|
|
||||||
implemented by public license practices. Many people have made
|
|
||||||
generous contributions to the wide range of software distributed
|
|
||||||
through that system in reliance on consistent application of that
|
|
||||||
system; it is up to the author/donor to decide if he or she is willing
|
|
||||||
to distribute software through any other system and a licensee cannot
|
|
||||||
impose that choice.
|
|
||||||
|
|
||||||
This section is intended to make thoroughly clear what is believed to
|
|
||||||
be a consequence of the rest of this License.
|
|
||||||
|
|
||||||
8. If the distribution and/or use of the Program is restricted in
|
|
||||||
certain countries either by patents or by copyrighted interfaces, the
|
|
||||||
original copyright holder who places the Program under this License
|
|
||||||
may add an explicit geographical distribution limitation excluding
|
|
||||||
those countries, so that distribution is permitted only in or among
|
|
||||||
countries not thus excluded. In such case, this License incorporates
|
|
||||||
the limitation as if written in the body of this License.
|
|
||||||
|
|
||||||
9. The Free Software Foundation may publish revised and/or new versions
|
|
||||||
of the General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the Program
|
|
||||||
specifies a version number of this License which applies to it and "any
|
|
||||||
later version", you have the option of following the terms and conditions
|
|
||||||
either of that version or of any later version published by the Free
|
|
||||||
Software Foundation. If the Program does not specify a version number of
|
|
||||||
this License, you may choose any version ever published by the Free Software
|
|
||||||
Foundation.
|
|
||||||
|
|
||||||
10. If you wish to incorporate parts of the Program into other free
|
|
||||||
programs whose distribution conditions are different, write to the author
|
|
||||||
to ask for permission. For software which is copyrighted by the Free
|
|
||||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
|
||||||
make exceptions for this. Our decision will be guided by the two goals
|
|
||||||
of preserving the free status of all derivatives of our free software and
|
|
||||||
of promoting the sharing and reuse of software generally.
|
|
||||||
|
|
||||||
NO WARRANTY
|
|
||||||
|
|
||||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
|
||||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
|
||||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
|
||||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
|
||||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
||||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
|
||||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
|
||||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
|
||||||
REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
|
||||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
|
||||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
|
||||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
|
||||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
|
||||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGES.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
convey the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program is interactive, make it output a short notice like this
|
|
||||||
when it starts in an interactive mode:
|
|
||||||
|
|
||||||
Gnomovision version 69, Copyright (C) year name of author
|
|
||||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, the commands you use may
|
|
||||||
be called something other than `show w' and `show c'; they could even be
|
|
||||||
mouse-clicks or menu items--whatever suits your program.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or your
|
|
||||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
|
||||||
necessary. Here is a sample; alter the names:
|
|
||||||
|
|
||||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
|
||||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
|
||||||
|
|
||||||
<signature of Ty Coon>, 1 April 1989
|
|
||||||
Ty Coon, President of Vice
|
|
||||||
|
|
||||||
This General Public License does not permit incorporating your program into
|
|
||||||
proprietary programs. If your program is a subroutine library, you may
|
|
||||||
consider it more useful to permit linking proprietary applications with the
|
|
||||||
library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License.
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
"plugins": [
|
||||||
|
"@babel/plugin-proposal-class-properties"
|
||||||
|
],
|
||||||
|
"presets": []
|
||||||
|
};
|
522
web/index.html
|
@ -1,522 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=8,IE=9,IE=10"><!-- ticket #4555 -->
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<link href="./images/favicon.ico" rel="icon" />
|
|
||||||
<link href="./images/favicon.png" rel="shortcut icon" />
|
|
||||||
<link rel="apple-touch-icon" href="./images/webclip-icon.png"/>
|
|
||||||
<script type="text/javascript" src="./javascript/jquery/jquery.min.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/jquery/jquery-migrate.min.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/jquery/jquery-ui.min.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/jquery/jquery.ui-contextmenu.min.js"></script>
|
|
||||||
<link rel="stylesheet" href="./style/jqueryui/jquery-ui.min.css" type="text/css" media="all" />
|
|
||||||
<!--
|
|
||||||
<link media="screen" href="./style/transmission/mobile.css" type= "text/css" rel="stylesheet" />
|
|
||||||
-->
|
|
||||||
<link media="only screen and (max-device-width: 480px)" href="./style/transmission/mobile.css" type= "text/css" rel="stylesheet" />
|
|
||||||
<link media="screen and (min-device-width: 481px)" href="./style/transmission/common.css" type="text/css" rel="stylesheet" />
|
|
||||||
<!--[if IE 8]>
|
|
||||||
<link media="screen" href="./style/transmission/common.css" type="text/css" rel="stylesheet" />
|
|
||||||
<![endif]-->
|
|
||||||
<script type="text/javascript" src="./javascript/jquery/jquery.transmenu.min.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/polyfill.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/common.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/inspector.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/prefs-dialog.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/remote.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/transmission.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/torrent.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/torrent-row.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/file-row.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/dialog.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/formatter.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/notifications.js"></script>
|
|
||||||
<script type="text/javascript" src="./javascript/main.js"></script>
|
|
||||||
<title>Transmission Web Interface</title>
|
|
||||||
</head>
|
|
||||||
<body id="transmission_body">
|
|
||||||
|
|
||||||
<div id="toolbar">
|
|
||||||
<div id="toolbar-open" title="Open Torrent"></div>
|
|
||||||
<div id="toolbar-remove" title="Remove Selected Torrents"></div>
|
|
||||||
<div class="toolbar-separator"></div>
|
|
||||||
<div id="toolbar-start" title="Start Selected Torrents"></div>
|
|
||||||
<div id="toolbar-pause" title="Pause Selected Torrents"></div>
|
|
||||||
<div class="toolbar-separator"></div>
|
|
||||||
<div id="toolbar-start-all" title="Start All Torrents"></div>
|
|
||||||
<div id="toolbar-pause-all" title="Pause All Torrents"></div>
|
|
||||||
<div id="toolbar-inspector" title="Toggle Inspector"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="statusbar">
|
|
||||||
<div id='filter'>
|
|
||||||
Show
|
|
||||||
<select id="filter-mode">
|
|
||||||
<option value="all">All</option>
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="downloading">Downloading</option>
|
|
||||||
<option value="seeding">Seeding</option>
|
|
||||||
<option value="paused">Paused</option>
|
|
||||||
<option value="finished">Finished</option>
|
|
||||||
</select>
|
|
||||||
<select id="filter-tracker"></select>
|
|
||||||
<input type="search" id="torrent_search" placeholder="Filter" />
|
|
||||||
<span id="filter-count"> </span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id='speed-info'>
|
|
||||||
<div id='speed-dn-container'>
|
|
||||||
<div id='speed-dn-icon'></div>
|
|
||||||
<div id='speed-dn-label'></div>
|
|
||||||
</div>
|
|
||||||
<div id='speed-up-container'>
|
|
||||||
<div id='speed-up-icon'></div>
|
|
||||||
<div id='speed-up-label'></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui-helper-hidden" id="prefs-dialog">
|
|
||||||
<ul>
|
|
||||||
<li id="prefs-tab-general"><a href="#prefs-page-torrents">Torrents</a></li>
|
|
||||||
<li id="prefs-tab-speed"><a href="#prefs-page-speed">Speed</a></li>
|
|
||||||
<li id="prefs-tab-peers"><a href="#prefs-page-peers">Peers</a></li>
|
|
||||||
<li id="prefs-tab-network"><a href="#prefs-page-network">Network</a></li>
|
|
||||||
<li class="ui-tab-dialog-close"></li>
|
|
||||||
</ul>
|
|
||||||
<div>
|
|
||||||
<div id="prefs-page-torrents">
|
|
||||||
<div class="prefs-section">
|
|
||||||
<div class="title">Downloading</div>
|
|
||||||
<div class="row"><div class="key">Download to:</div><div class="value"><input type="text" id="download-dir"/></div></div>
|
|
||||||
<div class="checkbox-row"><input type="checkbox" id="start-added-torrents"/><label for="start-added-torrents">Start when added</label></div>
|
|
||||||
<div class="checkbox-row"><input type="checkbox" id="rename-partial-files"/><label for="rename-partial-files">Append ".part" to incomplete files' names</label></div>
|
|
||||||
</div>
|
|
||||||
<div class="prefs-section">
|
|
||||||
<div class="title">Seeding</div>
|
|
||||||
<div class="row"><div class="key"><input type="checkbox" id="seedRatioLimited"/><label for="seedRatioLimited">Stop seeding at ratio:</label></div>
|
|
||||||
<div class="value"><input type="number" min="0" id="seedRatioLimit"/></div></div>
|
|
||||||
<div class="row"><div class="key"><input type="checkbox" id="idle-seeding-limit-enabled"/><label for="idle-seeding-limit-enabled">Stop seeding if idle for (min):</label></div>
|
|
||||||
<div class="value"><input type="number" min="1" max="40320" id="idle-seeding-limit"/></div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="prefs-page-speed">
|
|
||||||
<div class="prefs-section">
|
|
||||||
<div class="title">Speed Limits</div>
|
|
||||||
<div class="row"><div class="key"><input type="checkbox" id="speed-limit-up-enabled"/><label for="speed-limit-up-enabled">Upload (kB/s):</label></div>
|
|
||||||
<div class="value"><input type="number" min="0" id="speed-limit-up"/></div></div>
|
|
||||||
<div class="row"><div class="key"><input type="checkbox" id="speed-limit-down-enabled"/><label for="speed-limit-down-enabled">Download (kB/s):</label></div>
|
|
||||||
<div class="value"><input type="number" min="0" id="speed-limit-down"/></div></div>
|
|
||||||
</div>
|
|
||||||
<div class="prefs-section">
|
|
||||||
<div class="title"><div id="alternative-speed-limits-title">Alternative Speed Limits</div></div>
|
|
||||||
<div class="row" id="alternative-speed-limits-desc">Override normal speed limits manually or at scheduled times</div>
|
|
||||||
<div class="row"><div class="key">Upload (kB/s):</div>
|
|
||||||
<div class="value"><input type="number" min="0" id="alt-speed-up"/></div></div>
|
|
||||||
<div class="row"><div class="key">Download (kB/s):</div>
|
|
||||||
<div class="value"><input type="number" min="0" id="alt-speed-down"/></div></div>
|
|
||||||
<div class="checkbox-row"><input type="checkbox" id="alt-speed-time-enabled"/><label for="alt-speed-time-enabled">Scheduled Times</label></div>
|
|
||||||
<div class="row"><div class="key">From:</div>
|
|
||||||
<div class="value"><select id="alt-speed-time-begin"></select></div></div>
|
|
||||||
<div class="row"><div class="key">To:</div>
|
|
||||||
<div class="value"><select id="alt-speed-time-end"></select></div></div>
|
|
||||||
<div class="row"><div class="key"><label for="alt-speed-time-day">On days:</label></div>
|
|
||||||
<div class="value"><select id="alt-speed-time-day">
|
|
||||||
<option value="127">Everyday</option>
|
|
||||||
<option value="62">Weekdays</option>
|
|
||||||
<option value="65">Weekends</option>
|
|
||||||
<option value="1">Sunday</option>
|
|
||||||
<option value="2">Monday</option>
|
|
||||||
<option value="4">Tuesday</option>
|
|
||||||
<option value="8">Wednesday</option>
|
|
||||||
<option value="16">Thursday</option>
|
|
||||||
<option value="32">Friday</option>
|
|
||||||
<option value="64">Saturday</option></select></div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="prefs-page-peers">
|
|
||||||
<div class="prefs-section">
|
|
||||||
<div class="title">Connections</div>
|
|
||||||
<div class="row"><div class="key"><label for="peer-limit-per-torrent">Max peers per torrent:</label></div>
|
|
||||||
<div class="value"><input type="number" min="0" id="peer-limit-per-torrent"/></div></div>
|
|
||||||
<div class="row"><div class="key"><label for="peer-limit-global">Max peers overall:</label></div>
|
|
||||||
<div class="value"><input type="number" min="0" id="peer-limit-global"/></div></div>
|
|
||||||
</div>
|
|
||||||
<div class="prefs-section">
|
|
||||||
<div class="title">Options</div>
|
|
||||||
<div class="row"><div class="key">Encryption mode:</div>
|
|
||||||
<div class="value"><select id="encryption">
|
|
||||||
<option value="tolerated">Allow encryption</option>
|
|
||||||
<option value="preferred">Prefer encryption</option>
|
|
||||||
<option value="required">Require encryption</option></select></div></div>
|
|
||||||
<div class="checkbox-row"><input type="checkbox" id="pex-enabled" title="PEX is a tool for exchanging peer lists with the peers you're connected to."/>
|
|
||||||
<label for="pex-enabled" title="PEX is a tool for exchanging peer lists with the peers you're connected to.">Use PEX to find more peers</label></div>
|
|
||||||
<div class="checkbox-row"><input type="checkbox" id="dht-enabled" title="DHT is a tool for finding peers without a tracker."/>
|
|
||||||
<label for="dht-enabled" title="DHT is a tool for finding peers without a tracker.">Use DHT to find more peers</label></div>
|
|
||||||
<div class="checkbox-row"><input type="checkbox" id="lpd-enabled" title="LPD is a tool for finding peers on your local network."/>
|
|
||||||
<label for="lpd-enabled" title="LPD is a tool for finding peers on your local network.">Use LPD to find more peers</label></div>
|
|
||||||
</div>
|
|
||||||
<div class="prefs-section">
|
|
||||||
<div class="title">Blocklist</div>
|
|
||||||
<div class="row"><div class="key"><input type="checkbox" id="blocklist-enabled"/><label for="blocklist-enabled">Enable blocklist:</label></div>
|
|
||||||
<div class="value"><input type="url" id="blocklist-url"/></div></div>
|
|
||||||
<div class="row"><div class="key" id="blocklist-info">Blocklist has <span id="blocklist-size">?</span> rules</div>
|
|
||||||
<div class="value"><input type="button" id="blocklist-update-button" value="Update"/></div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="prefs-page-network">
|
|
||||||
<div class="prefs-section">
|
|
||||||
<div class="title">Listening Port</div>
|
|
||||||
<div class="row"><div class="key"><label for="peer-port">Peer listening port:</label></div>
|
|
||||||
<div class="value"><input type="number" min="0" max="65535" id="peer-port"/></div></div>
|
|
||||||
<div class="row"><div class="key"> </div>
|
|
||||||
<div class="value"><span id="port-label">Status: Unknown</span></div></div>
|
|
||||||
<div class="checkbox-row"><input type="checkbox" id="peer-port-random-on-start"/><label for="peer-port-random-on-start">Randomize port on launch</label></div>
|
|
||||||
<div class="checkbox-row"><input type="checkbox" id="port-forwarding-enabled"/><label for="port-forwarding-enabled">Use port forwarding from my router</label></div>
|
|
||||||
</div>
|
|
||||||
<div class="prefs-section">
|
|
||||||
<div class="title">Options</div>
|
|
||||||
<div class="checkbox-row"><input type="checkbox" id="utp-enabled" title="uTP is a tool for reducing network congestion."/>
|
|
||||||
<label for="utp-enabled" title="uTP is a tool for reducing network congestion.">Enable uTP for peer communication</label></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui-helper-hidden" id="torrent_inspector">
|
|
||||||
|
|
||||||
<div id="inspector-tabs-wrapper">
|
|
||||||
<div id="inspector-tabs">
|
|
||||||
<div class="inspector-tab selected" id="inspector-tab-info" title="Info"><a href="#info"></a></div><div class="inspector-tab" id="inspector-tab-peers" title="Peers"><a href="#peers"></a></div><div class="inspector-tab" id="inspector-tab-trackers" title="Trackers"><a href="#trackers"></a></div><div class="inspector-tab" id="inspector-tab-files" title="Files"><a href="#files"></a></div>
|
|
||||||
</div><!-- inspector-tabs -->
|
|
||||||
</div><!-- inspector-tabs-wrapper -->
|
|
||||||
|
|
||||||
<div id="inspector_header">
|
|
||||||
<div id="torrent_inspector_name"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="inspector-page" id="inspector-page-info">
|
|
||||||
<div class="prefs-section">
|
|
||||||
<div class="title">Activity</div>
|
|
||||||
<div class="row"><div class="key">Have:</div><div class="value" id="inspector-info-have"> </div></div>
|
|
||||||
<div class="row"><div class="key">Availability:</div><div class="value" id="inspector-info-availability"> </div></div>
|
|
||||||
<div class="row"><div class="key">Uploaded:</div><div class="value" id="inspector-info-uploaded"> </div></div>
|
|
||||||
<div class="row"><div class="key">Downloaded:</div><div class="value" id="inspector-info-downloaded"> </div></div>
|
|
||||||
<div class="row"><div class="key">State:</div><div class="value" id="inspector-info-state"> </div></div>
|
|
||||||
<div class="row"><div class="key">Running Time:</div><div class="value" id="inspector-info-running-time"> </div></div>
|
|
||||||
<div class="row"><div class="key">Remaining Time:</div><div class="value" id="inspector-info-remaining-time"> </div></div>
|
|
||||||
<div class="row"><div class="key">Last Activity:</div><div class="value" id="inspector-info-last-activity"> </div></div>
|
|
||||||
<div class="row"><div class="key">Error:</div><div class="value" id="inspector-info-error"> </div></div>
|
|
||||||
</div>
|
|
||||||
<div class="prefs-section">
|
|
||||||
<div class="title">Details</div>
|
|
||||||
<div class="row"><div class="key">Size:</div><div class="value" id="inspector-info-size"> </div></div>
|
|
||||||
<div class="row"><div class="key">Location:</div><div class="value" id="inspector-info-location"> </div></div>
|
|
||||||
<div class="row"><div class="key">Hash:</div><div class="value" id="inspector-info-hash"> </div></div>
|
|
||||||
<div class="row"><div class="key">Privacy:</div><div class="value" id="inspector-info-privacy"> </div></div>
|
|
||||||
<div class="row"><div class="key">Origin:</div><div class="value" id="inspector-info-origin"> </div></div>
|
|
||||||
<div class="row"><div class="key">Comment:</div><div class="value" id="inspector-info-comment"> </div></div>
|
|
||||||
</div>
|
|
||||||
</div><!-- id="inspector_tab_info_container" -->
|
|
||||||
|
|
||||||
<div class="inspector-page ui-helper-hidden" id="inspector-page-peers">
|
|
||||||
<div id="inspector_peers_list">
|
|
||||||
</div>
|
|
||||||
</div><!-- id="inspector_tab_peers_container" -->
|
|
||||||
|
|
||||||
<div class="inspector-page ui-helper-hidden" id="inspector-page-trackers">
|
|
||||||
<div id="inspector_trackers_list">
|
|
||||||
</div>
|
|
||||||
</div><!-- id="inspector_tab_trackers_container" -->
|
|
||||||
|
|
||||||
<div class="inspector-page ui-helper-hidden" id="inspector-page-files">
|
|
||||||
<ul id="inspector_file_list">
|
|
||||||
</ul>
|
|
||||||
</div><!-- id="inspector_tab_files_container" -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="torrent_container">
|
|
||||||
<ul class="torrent_list" id="torrent_list"></ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dialog_container ui-helper-hidden" id="dialog_container">
|
|
||||||
<div class="dialog_top_bar"></div>
|
|
||||||
<div class="dialog_window">
|
|
||||||
<div class="dialog_logo" id="dialog_logo"></div>
|
|
||||||
<h2 class="dialog_heading" id="dialog_heading"></h2>
|
|
||||||
<div class="dialog_message" id="dialog_message"></div>
|
|
||||||
<a href="#confirm" id="dialog_confirm_button">Confirm</a>
|
|
||||||
<a href="#cancel" id="dialog_cancel_button">Cancel</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui-helper-hidden" id="about-dialog">
|
|
||||||
<p id="about-logo"></p>
|
|
||||||
<p id="about-title">Transmission X</p>
|
|
||||||
<p id="about-blurb">A fast and easy BitTorrent client</p>
|
|
||||||
<p id="about-copyright">Copyright (c) The Transmission Project</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui-helper-hidden" id="stats-dialog">
|
|
||||||
<div class="prefs-section">
|
|
||||||
<div class="title">Current Session</div>
|
|
||||||
<div class="row"><div class="key">Uploaded:</div><div class="value" id='stats-session-uploaded'> </div></div>
|
|
||||||
<div class="row"><div class="key">Downloaded:</div><div class="value" id='stats-session-downloaded'> </div></div>
|
|
||||||
<div class="row"><div class="key">Ratio:</div><div class="value" id='stats-session-ratio'> </div></div>
|
|
||||||
<div class="row"><div class="key">Running Time:</div><div class="value" id='stats-session-duration'> </div></div>
|
|
||||||
</div>
|
|
||||||
<div class="prefs-section">
|
|
||||||
<div class="title">Total</div>
|
|
||||||
<div class="row"><div class="key">Started:</div><div class="value" id='stats-total-count'> </div></div>
|
|
||||||
<div class="row"><div class="key">Uploaded:</div><div class="value" id='stats-total-uploaded'> </div></div>
|
|
||||||
<div class="row"><div class="key">Downloaded:</div><div class="value" id='stats-total-downloaded'> </div></div>
|
|
||||||
<div class="row"><div class="key">Ratio:</div><div class="value" id='stats-total-ratio'> </div></div>
|
|
||||||
<div class="row"><div class="key">Running Time:</div><div class="value" id='stats-total-duration'> </div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui-helper-hidden" id="hotkeys-dialog">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Key</th>
|
|
||||||
<th>Action</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>/</td>
|
|
||||||
<td>Show hotkeys dialog</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>,</td>
|
|
||||||
<td>Open preferences</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Enter</td>
|
|
||||||
<td>Confirm dialog</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>ESC</td>
|
|
||||||
<td>Close/cancel dialog</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>a</td>
|
|
||||||
<td>Select all torrents</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>SHIFT + a</td>
|
|
||||||
<td>Deselect all torrents</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>c</td>
|
|
||||||
<td>Toggle compact view</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>d</td>
|
|
||||||
<td>Delete selected torrents</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Backspace</td>
|
|
||||||
<td>Delete selected torrents</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>DEL</td>
|
|
||||||
<td>Delete selected torrents</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>i</td>
|
|
||||||
<td>Toggle inspector</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>l</td>
|
|
||||||
<td>Move torrent/Set location</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>m</td>
|
|
||||||
<td>Move torrent/Set location</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>o</td>
|
|
||||||
<td>Add/open a torrent</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>u</td>
|
|
||||||
<td>Add/open a torrent</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>p</td>
|
|
||||||
<td>Pause selected torrents</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>r</td>
|
|
||||||
<td>Resume selected torrents</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>t</td>
|
|
||||||
<td>Toggle turtle mode</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dialog_container ui-helper-hidden" id="upload_container">
|
|
||||||
<div class="dialog_top_bar"></div>
|
|
||||||
<div class="dialog_window">
|
|
||||||
<div class="dialog_logo" id="upload_dialog_logo"></div>
|
|
||||||
<h2 class="dialog_heading">Upload Torrent Files</h2>
|
|
||||||
<form action="#" method="post" id="torrent_upload_form"
|
|
||||||
enctype="multipart/form-data" target="torrent_upload_frame" autocomplete="off">
|
|
||||||
<div class="dialog_message">
|
|
||||||
<label for="torrent_upload_file">Please select a torrent file to upload:</label>
|
|
||||||
<input type="file" name="torrent_files[]" id="torrent_upload_file" multiple="multiple" />
|
|
||||||
<label for="torrent_upload_url">Or enter a URL:</label>
|
|
||||||
<input type="url" id="torrent_upload_url"/>
|
|
||||||
<label id='add-dialog-folder-label' for="add-dialog-folder-input">Destination folder:</label>
|
|
||||||
<input type="text" id="add-dialog-folder-input"/>
|
|
||||||
<input type="checkbox" id="torrent_auto_start" />
|
|
||||||
<label for="torrent_auto_start" id="auto_start_label">Start when added</label>
|
|
||||||
</div>
|
|
||||||
<a href="#upload" id="upload_confirm_button">Upload</a>
|
|
||||||
<a href="#cancel" id="upload_cancel_button">Cancel</a>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dialog_container ui-helper-hidden" id="rename_container">
|
|
||||||
<div class="dialog_top_bar"></div>
|
|
||||||
<div class="dialog_window">
|
|
||||||
<div class="dialog_logo"></div>
|
|
||||||
<h2 class="dialog_heading">Rename torrent</h2>
|
|
||||||
<div class="dialog_message">
|
|
||||||
<label for="torrent_rename_name">Enter new name:</label>
|
|
||||||
<input type="text" id="torrent_rename_name"/>
|
|
||||||
</div>
|
|
||||||
<a href="#rename" id="rename_confirm_button">Rename</a>
|
|
||||||
<a href="#cancel" id="rename_cancel_button">Cancel</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dialog_container ui-helper-hidden" id="move_container">
|
|
||||||
<div class="dialog_top_bar"></div>
|
|
||||||
<div class="dialog_window">
|
|
||||||
<div class="dialog_logo" id="move_dialog_logo"></div>
|
|
||||||
<h2 class="dialog_heading">Set Location</h2>
|
|
||||||
<form action="#" method="post" id="torrent_move_form"
|
|
||||||
enctype="multipart/form-data" target="torrent_move_frame">
|
|
||||||
<div class="dialog_message">
|
|
||||||
<label for="torrent_path">Location:</label>
|
|
||||||
<input type="text" id="torrent_path"/>
|
|
||||||
</div>
|
|
||||||
<a href="#move" id="move_confirm_button">Apply</a>
|
|
||||||
<a href="#cancel" id="move_cancel_button">Cancel</a>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="torrent_footer">
|
|
||||||
<div id="settings_menu" title="Settings Menu"> </div>
|
|
||||||
<div id="prefs-button" title="Edit Preferences…"> </div>
|
|
||||||
<div id="turtle-button" title="Alternative Speed Limits"> </div>
|
|
||||||
<div id="compact-button" title="Compact View"> </div>
|
|
||||||
|
|
||||||
<ul class="ui-helper-hidden" id="footer_super_menu">
|
|
||||||
<li id="about-button">About</li>
|
|
||||||
<li>---</li>
|
|
||||||
<li id="homepage">Transmission Homepage</li>
|
|
||||||
<li id="tipjar">Transmission Tip Jar</li>
|
|
||||||
<li>---</li>
|
|
||||||
<li id="statistics">Statistics</li>
|
|
||||||
<li id="hotkeys">Hotkeys</li>
|
|
||||||
<!--
|
|
||||||
<li id="toggle_notifications">Notifcations</li>
|
|
||||||
-->
|
|
||||||
<li>---</li>
|
|
||||||
<li>Total Download Rate
|
|
||||||
<ul id="footer_download_rate_menu">
|
|
||||||
<li radio-group="download-rate" id="unlimited_download_rate"><span class='ui-icon ui-icon-bullet'></span>Unlimited</li>
|
|
||||||
<li radio-group="download-rate" id="limited_download_rate">Limit (10 kB/s)</li>
|
|
||||||
<li>---</li>
|
|
||||||
<li class='download-speed'>5 kB/s</li>
|
|
||||||
<li class='download-speed'>10 kB/s</li>
|
|
||||||
<li class='download-speed'>20 kB/s</li>
|
|
||||||
<li class='download-speed'>30 kB/s</li>
|
|
||||||
<li class='download-speed'>40 kB/s</li>
|
|
||||||
<li class='download-speed'>50 kB/s</li>
|
|
||||||
<li class='download-speed'>75 kB/s</li>
|
|
||||||
<li class='download-speed'>100 kB/s</li>
|
|
||||||
<li class='download-speed'>150 kB/s</li>
|
|
||||||
<li class='download-speed'>200 kB/s</li>
|
|
||||||
<li class='download-speed'>250 kB/s</li>
|
|
||||||
<li class='download-speed'>500 kB/s</li>
|
|
||||||
<li class='download-speed'>750 kB/s</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>Total Upload Rate
|
|
||||||
<ul id="footer_upload_rate_menu">
|
|
||||||
<li radio-group="upload-rate" id="unlimited_upload_rate"><span class='ui-icon ui-icon-bullet'></span>Unlimited</li>
|
|
||||||
<li radio-group="upload-rate" id="limited_upload_rate">Limit (10 kB/s)</li>
|
|
||||||
<li>---</li>
|
|
||||||
<li class='upload-speed'>5 kB/s</li>
|
|
||||||
<li class='upload-speed'>10 kB/s</li>
|
|
||||||
<li class='upload-speed'>20 kB/s</li>
|
|
||||||
<li class='upload-speed'>30 kB/s</li>
|
|
||||||
<li class='upload-speed'>40 kB/s</li>
|
|
||||||
<li class='upload-speed'>50 kB/s</li>
|
|
||||||
<li class='upload-speed'>75 kB/s</li>
|
|
||||||
<li class='upload-speed'>100 kB/s</li>
|
|
||||||
<li class='upload-speed'>150 kB/s</li>
|
|
||||||
<li class='upload-speed'>200 kB/s</li>
|
|
||||||
<li class='upload-speed'>250 kB/s</li>
|
|
||||||
<li class='upload-speed'>500 kB/s</li>
|
|
||||||
<li class='upload-speed'>750 kB/s</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>---</li>
|
|
||||||
<li>Sort Transfers By
|
|
||||||
<ul id="footer_sort_menu">
|
|
||||||
<li radio-group="sort-mode" id="sort_by_queue_order">Queue Order</li>
|
|
||||||
<li radio-group="sort-mode" id="sort_by_activity">Activity</li>
|
|
||||||
<li radio-group="sort-mode" id="sort_by_age">Age</li>
|
|
||||||
<li radio-group="sort-mode" id="sort_by_name">Name</li>
|
|
||||||
<li radio-group="sort-mode" id="sort_by_percent_completed">Progress</li>
|
|
||||||
<li radio-group="sort-mode" id="sort_by_ratio">Ratio</li>
|
|
||||||
<li radio-group="sort-mode" id="sort_by_size">Size</li>
|
|
||||||
<li radio-group="sort-mode" id="sort_by_state">State</li>
|
|
||||||
<li>---</li>
|
|
||||||
<li id="reverse_sort_order">Reverse Sort Order</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="ui-helper-hidden" id="torrent_context_menu">
|
|
||||||
<li data-command="pause_selected">Pause</li>
|
|
||||||
<li data-command="resume_selected">Resume</li>
|
|
||||||
<li data-command="resume_now_selected">Resume Now</li>
|
|
||||||
<li>---</li>
|
|
||||||
<li data-command="move_top">Move to Top</li>
|
|
||||||
<li data-command="move_up">Move Up</li>
|
|
||||||
<li data-command="move_down">Move Down</li>
|
|
||||||
<li data-command="move_bottom">Move to Bottom</li>
|
|
||||||
<li>---</li>
|
|
||||||
<li data-command="remove">Remove From List…</li>
|
|
||||||
<li data-command="remove_data">Trash Data and Remove From List…</li>
|
|
||||||
<li>---</li>
|
|
||||||
<li data-command="verify">Verify Local Data</li>
|
|
||||||
<li data-command="move">Set Location…</li>
|
|
||||||
<li data-command="rename">Rename…</li>
|
|
||||||
<li>---</li>
|
|
||||||
<li data-command="reannounce">Ask tracker for more peers</li>
|
|
||||||
<li>---</li>
|
|
||||||
<li data-command="select_all">Select All</li>
|
|
||||||
<li data-command="deselect_all">Deselect All</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<iframe name="torrent_upload_frame" id="torrent_upload_frame" src="about:blank" ></iframe>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,240 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright © Dave Perrett, Malcolm Jarvis and Artem Vorotnikov
|
|
||||||
*
|
|
||||||
* This file is licensed under the GPLv2.
|
|
||||||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
let transmission,
|
|
||||||
dialog,
|
|
||||||
isMobileDevice = RegExp('(iPhone|iPod|Android)').test(navigator.userAgent),
|
|
||||||
scroll_timeout;
|
|
||||||
|
|
||||||
// http://forum.jquery.com/topic/combining-ui-dialog-and-tabs
|
|
||||||
$.fn.tabbedDialog = function (dialog_opts) {
|
|
||||||
this.tabs({
|
|
||||||
selected: 0,
|
|
||||||
});
|
|
||||||
this.dialog(dialog_opts);
|
|
||||||
this.find('.ui-tab-dialog-close').append(this.parent().find('.ui-dialog-titlebar-close'));
|
|
||||||
this.find('.ui-tab-dialog-close').css({
|
|
||||||
position: 'absolute',
|
|
||||||
right: '0',
|
|
||||||
top: '16px',
|
|
||||||
});
|
|
||||||
this.find('.ui-tab-dialog-close > a').css({
|
|
||||||
float: 'none',
|
|
||||||
padding: '0',
|
|
||||||
});
|
|
||||||
const tabul = this.find('ul:first');
|
|
||||||
this.parent().addClass('ui-tabs').prepend(tabul).draggable('option', 'handle', tabul);
|
|
||||||
this.siblings('.ui-dialog-titlebar').remove();
|
|
||||||
tabul.addClass('ui-dialog-titlebar');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks to see if the content actually changed before poking the DOM.
|
|
||||||
*/
|
|
||||||
function setInnerHTML(e, html) {
|
|
||||||
if (!e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* innerHTML is listed as a string, but the browser seems to change it.
|
|
||||||
* For example, "∞" gets changed to "∞" somewhere down the line.
|
|
||||||
* So, let's use an arbitrary different field to test our state... */
|
|
||||||
if (e.currentHTML != html) {
|
|
||||||
e.currentHTML = html;
|
|
||||||
e.innerHTML = html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeText(text) {
|
|
||||||
return text.replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Many of our text changes are triggered by periodic refreshes
|
|
||||||
* on torrents whose state hasn't changed since the last update,
|
|
||||||
* so see if the text actually changed before poking the DOM.
|
|
||||||
*/
|
|
||||||
function setTextContent(e, text) {
|
|
||||||
if (e && e.textContent != text) {
|
|
||||||
e.textContent = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Given a numerator and denominator, return a ratio string
|
|
||||||
*/
|
|
||||||
Math.ratio = function (numerator, denominator) {
|
|
||||||
let result = Math.floor((100 * numerator) / denominator) / 100;
|
|
||||||
|
|
||||||
// check for special cases
|
|
||||||
if (result == Number.POSITIVE_INFINITY || result == Number.NEGATIVE_INFINITY) {
|
|
||||||
result = -2;
|
|
||||||
} else if (isNaN(result)) {
|
|
||||||
result = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Round a string of a number to a specified number of decimal places
|
|
||||||
*/
|
|
||||||
Number.prototype.toTruncFixed = function (place) {
|
|
||||||
const ret = Math.floor(this * Math.pow(10, place)) / Math.pow(10, place);
|
|
||||||
return ret.toFixed(place);
|
|
||||||
};
|
|
||||||
|
|
||||||
Number.prototype.toStringWithCommas = function () {
|
|
||||||
return this.toString().replace(/\B(?=(?:\d{3})+(?!\d))/g, ',');
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Trim whitespace from a string
|
|
||||||
*/
|
|
||||||
String.prototype.trim = function () {
|
|
||||||
return this.replace(/^\s*/, '').replace(/\s*$/, '');
|
|
||||||
};
|
|
||||||
|
|
||||||
/***
|
|
||||||
**** Preferences
|
|
||||||
***/
|
|
||||||
|
|
||||||
function Prefs() {}
|
|
||||||
Prefs.prototype = {};
|
|
||||||
|
|
||||||
Prefs._RefreshRate = 'refresh_rate';
|
|
||||||
|
|
||||||
Prefs._FilterMode = 'filter';
|
|
||||||
Prefs._FilterAll = 'all';
|
|
||||||
Prefs._FilterActive = 'active';
|
|
||||||
Prefs._FilterSeeding = 'seeding';
|
|
||||||
Prefs._FilterDownloading = 'downloading';
|
|
||||||
Prefs._FilterPaused = 'paused';
|
|
||||||
Prefs._FilterFinished = 'finished';
|
|
||||||
|
|
||||||
Prefs._SortDirection = 'sort_direction';
|
|
||||||
Prefs._SortAscending = 'ascending';
|
|
||||||
Prefs._SortDescending = 'descending';
|
|
||||||
|
|
||||||
Prefs._SortMethod = 'sort_method';
|
|
||||||
Prefs._SortByAge = 'age';
|
|
||||||
Prefs._SortByActivity = 'activity';
|
|
||||||
Prefs._SortByName = 'name';
|
|
||||||
Prefs._SortByQueue = 'queue_order';
|
|
||||||
Prefs._SortBySize = 'size';
|
|
||||||
Prefs._SortByProgress = 'percent_completed';
|
|
||||||
Prefs._SortByRatio = 'ratio';
|
|
||||||
Prefs._SortByState = 'state';
|
|
||||||
|
|
||||||
Prefs._CompactDisplayState = 'compact_display_state';
|
|
||||||
|
|
||||||
Prefs._Defaults = {
|
|
||||||
filter: 'all',
|
|
||||||
refresh_rate: 5,
|
|
||||||
sort_direction: 'ascending',
|
|
||||||
sort_method: 'name',
|
|
||||||
'turtle-state': false,
|
|
||||||
compact_display_state: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Set a preference option
|
|
||||||
*/
|
|
||||||
Prefs.setValue = function (key, val) {
|
|
||||||
if (!(key in Prefs._Defaults)) {
|
|
||||||
console.warn("unrecognized preference key '%s'", key);
|
|
||||||
}
|
|
||||||
|
|
||||||
const date = new Date();
|
|
||||||
date.setFullYear(date.getFullYear() + 1);
|
|
||||||
document.cookie = key + '=' + val + '; expires=' + date.toGMTString() + '; path=/';
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a preference option
|
|
||||||
*
|
|
||||||
* @param key the preference's key
|
|
||||||
* @param fallback if the option isn't set, return this instead
|
|
||||||
*/
|
|
||||||
Prefs.getValue = function (key, fallback) {
|
|
||||||
let val;
|
|
||||||
|
|
||||||
if (!(key in Prefs._Defaults)) {
|
|
||||||
console.warn("unrecognized preference key '%s'", key);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = document.cookie.split(';');
|
|
||||||
for (let i = 0, len = lines.length; !val && i < len; ++i) {
|
|
||||||
const line = lines[i].trim();
|
|
||||||
const delim = line.indexOf('=');
|
|
||||||
if (delim === key.length && line.indexOf(key) === 0) {
|
|
||||||
val = line.substring(delim + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: we support strings and booleans... add number support too?
|
|
||||||
if (!val) {
|
|
||||||
val = fallback;
|
|
||||||
} else if (val === 'true') {
|
|
||||||
val = true;
|
|
||||||
} else if (val === 'false') {
|
|
||||||
val = false;
|
|
||||||
}
|
|
||||||
return val;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an object with all the Clutch preferences set
|
|
||||||
*
|
|
||||||
* @pararm o object to be populated (optional)
|
|
||||||
*/
|
|
||||||
Prefs.getClutchPrefs = function (o) {
|
|
||||||
if (!o) {
|
|
||||||
o = {};
|
|
||||||
}
|
|
||||||
for (const key in Prefs._Defaults) {
|
|
||||||
o[key] = Prefs.getValue(key, Prefs._Defaults[key]);
|
|
||||||
}
|
|
||||||
return o;
|
|
||||||
};
|
|
||||||
|
|
||||||
// forceNumeric() plug-in implementation
|
|
||||||
jQuery.fn.forceNumeric = function () {
|
|
||||||
return this.each(function () {
|
|
||||||
$(this).keydown(function (e) {
|
|
||||||
const key = e.which || e.keyCode;
|
|
||||||
return (
|
|
||||||
(!e.shiftKey &&
|
|
||||||
!e.altKey &&
|
|
||||||
!e.ctrlKey &&
|
|
||||||
// numbers
|
|
||||||
key >= 48 &&
|
|
||||||
key <= 57) ||
|
|
||||||
// Numeric keypad
|
|
||||||
(key >= 96 && key <= 105) ||
|
|
||||||
// comma, period and minus, . on keypad
|
|
||||||
key === 190 ||
|
|
||||||
key === 188 ||
|
|
||||||
key === 109 ||
|
|
||||||
key === 110 ||
|
|
||||||
// Backspace and Tab and Enter
|
|
||||||
key === 8 ||
|
|
||||||
key === 9 ||
|
|
||||||
key === 13 ||
|
|
||||||
// Home and End
|
|
||||||
key === 35 ||
|
|
||||||
key === 36 ||
|
|
||||||
// left and right arrows
|
|
||||||
key === 37 ||
|
|
||||||
key === 39 ||
|
|
||||||
// Del and Ins
|
|
||||||
key === 46 ||
|
|
||||||
key === 45
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,129 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright © Dave Perrett and Malcolm Jarvis
|
|
||||||
*
|
|
||||||
* This file is licensed under the GPLv2.
|
|
||||||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Dialog() {
|
|
||||||
this.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
Dialog.prototype = {
|
|
||||||
/*
|
|
||||||
* Constructor
|
|
||||||
*/
|
|
||||||
initialize: function () {
|
|
||||||
/*
|
|
||||||
* Private Interface Variables
|
|
||||||
*/
|
|
||||||
this._container = $('#dialog_container');
|
|
||||||
this._heading = $('#dialog_heading');
|
|
||||||
this._message = $('#dialog_message');
|
|
||||||
this._cancel_button = $('#dialog_cancel_button');
|
|
||||||
this._confirm_button = $('#dialog_confirm_button');
|
|
||||||
this._callback = null;
|
|
||||||
|
|
||||||
// Observe the buttons
|
|
||||||
this._cancel_button.bind(
|
|
||||||
'click',
|
|
||||||
{
|
|
||||||
dialog: this,
|
|
||||||
},
|
|
||||||
this.onCancelClicked
|
|
||||||
);
|
|
||||||
this._confirm_button.bind(
|
|
||||||
'click',
|
|
||||||
{
|
|
||||||
dialog: this,
|
|
||||||
},
|
|
||||||
this.onConfirmClicked
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
/*--------------------------------------------
|
|
||||||
*
|
|
||||||
* E V E N T F U N C T I O N S
|
|
||||||
*
|
|
||||||
*--------------------------------------------*/
|
|
||||||
|
|
||||||
executeCallback: function () {
|
|
||||||
this._callback();
|
|
||||||
dialog.hideDialog();
|
|
||||||
},
|
|
||||||
|
|
||||||
hideDialog: function () {
|
|
||||||
$('body.dialog_showing').removeClass('dialog_showing');
|
|
||||||
this._container.hide();
|
|
||||||
transmission.hideMobileAddressbar();
|
|
||||||
transmission.updateButtonStates();
|
|
||||||
},
|
|
||||||
|
|
||||||
isVisible: function () {
|
|
||||||
return this._container.is(':visible');
|
|
||||||
},
|
|
||||||
|
|
||||||
onCancelClicked: function (event) {
|
|
||||||
event.data.dialog.hideDialog();
|
|
||||||
},
|
|
||||||
|
|
||||||
onConfirmClicked: function (event) {
|
|
||||||
event.data.dialog.executeCallback();
|
|
||||||
},
|
|
||||||
|
|
||||||
/*--------------------------------------------
|
|
||||||
*
|
|
||||||
* I N T E R F A C E F U N C T I O N S
|
|
||||||
*
|
|
||||||
*--------------------------------------------*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Display a confirm dialog
|
|
||||||
*/
|
|
||||||
confirm: function (
|
|
||||||
dialog_heading,
|
|
||||||
dialog_message,
|
|
||||||
confirm_button_label,
|
|
||||||
callback,
|
|
||||||
cancel_button_label
|
|
||||||
) {
|
|
||||||
if (!isMobileDevice) {
|
|
||||||
$('.dialog_container').hide();
|
|
||||||
}
|
|
||||||
setTextContent(this._heading[0], dialog_heading);
|
|
||||||
setTextContent(this._message[0], dialog_message);
|
|
||||||
setTextContent(this._cancel_button[0], cancel_button_label || 'Cancel');
|
|
||||||
setTextContent(this._confirm_button[0], confirm_button_label);
|
|
||||||
this._confirm_button.show();
|
|
||||||
this._callback = callback;
|
|
||||||
$('body').addClass('dialog_showing');
|
|
||||||
this._container.show();
|
|
||||||
transmission.updateButtonStates();
|
|
||||||
if (isMobileDevice) {
|
|
||||||
transmission.hideMobileAddressbar();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Display an alert dialog
|
|
||||||
*/
|
|
||||||
alert: function (dialog_heading, dialog_message, cancel_button_label) {
|
|
||||||
if (!isMobileDevice) {
|
|
||||||
$('.dialog_container').hide();
|
|
||||||
}
|
|
||||||
setTextContent(this._heading[0], dialog_heading);
|
|
||||||
setTextContent(this._message[0], dialog_message);
|
|
||||||
// jquery::hide() doesn't work here in Safari for some odd reason
|
|
||||||
this._confirm_button.css('display', 'none');
|
|
||||||
setTextContent(this._cancel_button[0], cancel_button_label);
|
|
||||||
// Just in case
|
|
||||||
$('#upload_container').hide();
|
|
||||||
$('#move_container').hide();
|
|
||||||
$('body').addClass('dialog_showing');
|
|
||||||
transmission.updateButtonStates();
|
|
||||||
if (isMobileDevice) {
|
|
||||||
transmission.hideMobileAddressbar();
|
|
||||||
}
|
|
||||||
this._container.show();
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,212 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright © Mnemosyne LLC
|
|
||||||
*
|
|
||||||
* This file is licensed under the GPLv2.
|
|
||||||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
function FileRow(torrent, depth, name, indices, even) {
|
|
||||||
const fields = {
|
|
||||||
have: 0,
|
|
||||||
indices: [],
|
|
||||||
isWanted: true,
|
|
||||||
priorityLow: false,
|
|
||||||
priorityNormal: false,
|
|
||||||
priorityHigh: false,
|
|
||||||
me: this,
|
|
||||||
size: 0,
|
|
||||||
torrent: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const elements = {
|
|
||||||
priority_low_button: null,
|
|
||||||
priority_normal_button: null,
|
|
||||||
priority_high_button: null,
|
|
||||||
progress: null,
|
|
||||||
root: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialize = function (torrent, depth, name, indices, even) {
|
|
||||||
fields.torrent = torrent;
|
|
||||||
fields.indices = indices;
|
|
||||||
createRow(torrent, depth, name, even);
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshWantedHTML = function () {
|
|
||||||
const e = $(elements.root);
|
|
||||||
e.toggleClass('skip', !fields.isWanted);
|
|
||||||
e.toggleClass('complete', isDone());
|
|
||||||
$(e[0].checkbox).prop('disabled', !isEditable());
|
|
||||||
$(e[0].checkbox).prop('checked', fields.isWanted);
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshProgressHTML = function () {
|
|
||||||
const pct = 100 * (fields.size ? fields.have / fields.size : 1.0);
|
|
||||||
const c = [
|
|
||||||
Transmission.fmt.size(fields.have),
|
|
||||||
' of ',
|
|
||||||
Transmission.fmt.size(fields.size),
|
|
||||||
' (',
|
|
||||||
Transmission.fmt.percentString(pct),
|
|
||||||
'%)',
|
|
||||||
].join('');
|
|
||||||
setTextContent(elements.progress, c);
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshImpl = function () {
|
|
||||||
let i,
|
|
||||||
file,
|
|
||||||
have = 0,
|
|
||||||
size = 0,
|
|
||||||
wanted = false,
|
|
||||||
low = false,
|
|
||||||
normal = false,
|
|
||||||
high = false;
|
|
||||||
|
|
||||||
// loop through the file_indices that affect this row
|
|
||||||
for (i = 0; i < fields.indices.length; ++i) {
|
|
||||||
file = fields.torrent.getFile(fields.indices[i]);
|
|
||||||
have += file.bytesCompleted;
|
|
||||||
size += file.length;
|
|
||||||
wanted |= file.wanted;
|
|
||||||
switch (file.priority) {
|
|
||||||
case -1:
|
|
||||||
low = true;
|
|
||||||
break;
|
|
||||||
case 0:
|
|
||||||
normal = true;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
high = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fields.have != have || fields.size != size) {
|
|
||||||
fields.have = have;
|
|
||||||
fields.size = size;
|
|
||||||
refreshProgressHTML();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fields.isWanted !== wanted) {
|
|
||||||
fields.isWanted = wanted;
|
|
||||||
refreshWantedHTML();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fields.priorityLow !== low) {
|
|
||||||
fields.priorityLow = low;
|
|
||||||
$(elements.priority_low_button).toggleClass('selected', low);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fields.priorityNormal !== normal) {
|
|
||||||
fields.priorityNormal = normal;
|
|
||||||
$(elements.priority_normal_button).toggleClass('selected', normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fields.priorityHigh !== high) {
|
|
||||||
fields.priorityHigh = high;
|
|
||||||
$(elements.priority_high_button).toggleClass('selected', high);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var isDone = function () {
|
|
||||||
return fields.have >= fields.size;
|
|
||||||
};
|
|
||||||
|
|
||||||
var isEditable = function () {
|
|
||||||
return fields.torrent.getFileCount() > 1 && !isDone();
|
|
||||||
};
|
|
||||||
|
|
||||||
var createRow = function (torrent, depth, name, even) {
|
|
||||||
let e, root, box;
|
|
||||||
|
|
||||||
root = document.createElement('li');
|
|
||||||
root.className = 'inspector_torrent_file_list_entry' + (even ? 'even' : 'odd');
|
|
||||||
elements.root = root;
|
|
||||||
|
|
||||||
e = document.createElement('input');
|
|
||||||
e.type = 'checkbox';
|
|
||||||
e.className = 'file_wanted_control';
|
|
||||||
e.title = 'Download file';
|
|
||||||
$(e).change(function (ev) {
|
|
||||||
fireWantedChanged($(ev.currentTarget).prop('checked'));
|
|
||||||
});
|
|
||||||
root.checkbox = e;
|
|
||||||
root.appendChild(e);
|
|
||||||
|
|
||||||
e = document.createElement('div');
|
|
||||||
e.className = 'file-priority-radiobox';
|
|
||||||
box = e;
|
|
||||||
|
|
||||||
e = document.createElement('div');
|
|
||||||
e.className = 'low';
|
|
||||||
e.title = 'Low Priority';
|
|
||||||
$(e).click(function () {
|
|
||||||
firePriorityChanged(-1);
|
|
||||||
});
|
|
||||||
elements.priority_low_button = e;
|
|
||||||
box.appendChild(e);
|
|
||||||
|
|
||||||
e = document.createElement('div');
|
|
||||||
e.className = 'normal';
|
|
||||||
e.title = 'Normal Priority';
|
|
||||||
$(e).click(function () {
|
|
||||||
firePriorityChanged(0);
|
|
||||||
});
|
|
||||||
elements.priority_normal_button = e;
|
|
||||||
box.appendChild(e);
|
|
||||||
|
|
||||||
e = document.createElement('div');
|
|
||||||
e.title = 'High Priority';
|
|
||||||
e.className = 'high';
|
|
||||||
$(e).click(function () {
|
|
||||||
firePriorityChanged(1);
|
|
||||||
});
|
|
||||||
elements.priority_high_button = e;
|
|
||||||
box.appendChild(e);
|
|
||||||
|
|
||||||
root.appendChild(box);
|
|
||||||
|
|
||||||
e = document.createElement('div');
|
|
||||||
e.className = 'inspector_torrent_file_list_entry_name';
|
|
||||||
setTextContent(e, name);
|
|
||||||
$(e).click(fireNameClicked);
|
|
||||||
root.appendChild(e);
|
|
||||||
|
|
||||||
e = document.createElement('div');
|
|
||||||
e.className = 'inspector_torrent_file_list_entry_progress';
|
|
||||||
root.appendChild(e);
|
|
||||||
$(e).click(fireNameClicked);
|
|
||||||
elements.progress = e;
|
|
||||||
|
|
||||||
$(root).css('margin-left', '' + depth * 16 + 'px');
|
|
||||||
|
|
||||||
refreshImpl();
|
|
||||||
return root;
|
|
||||||
};
|
|
||||||
|
|
||||||
var fireWantedChanged = function (do_want) {
|
|
||||||
$(fields.me).trigger('wantedToggled', [fields.indices, do_want]);
|
|
||||||
};
|
|
||||||
|
|
||||||
var firePriorityChanged = function (priority) {
|
|
||||||
$(fields.me).trigger('priorityToggled', [fields.indices, priority]);
|
|
||||||
};
|
|
||||||
|
|
||||||
var fireNameClicked = function () {
|
|
||||||
$(fields.me).trigger('nameClicked', [fields.me, fields.indices]);
|
|
||||||
};
|
|
||||||
|
|
||||||
/***
|
|
||||||
**** PUBLIC
|
|
||||||
***/
|
|
||||||
|
|
||||||
this.getElement = function () {
|
|
||||||
return elements.root;
|
|
||||||
};
|
|
||||||
this.refresh = function () {
|
|
||||||
refreshImpl();
|
|
||||||
};
|
|
||||||
|
|
||||||
initialize(torrent, depth, name, indices, even);
|
|
||||||
}
|
|
|
@ -1,301 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright © Mnemosyne LLC
|
|
||||||
*
|
|
||||||
* This file is licensed under the GPLv2.
|
|
||||||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
Transmission.fmt = (function () {
|
|
||||||
const speed_K = 1000;
|
|
||||||
const speed_K_str = 'kB/s';
|
|
||||||
const speed_M_str = 'MB/s';
|
|
||||||
const speed_G_str = 'GB/s';
|
|
||||||
|
|
||||||
const size_K = 1000;
|
|
||||||
const size_B_str = 'B';
|
|
||||||
const size_K_str = 'kB';
|
|
||||||
const size_M_str = 'MB';
|
|
||||||
const size_G_str = 'GB';
|
|
||||||
const size_T_str = 'TB';
|
|
||||||
|
|
||||||
const mem_K = 1024;
|
|
||||||
const mem_B_str = 'B';
|
|
||||||
const mem_K_str = 'KiB';
|
|
||||||
const mem_M_str = 'MiB';
|
|
||||||
const mem_G_str = 'GiB';
|
|
||||||
const mem_T_str = 'TiB';
|
|
||||||
|
|
||||||
return {
|
|
||||||
/*
|
|
||||||
* Format a percentage to a string
|
|
||||||
*/
|
|
||||||
percentString: function (x) {
|
|
||||||
if (x < 10.0) {
|
|
||||||
return x.toTruncFixed(2);
|
|
||||||
} else if (x < 100.0) {
|
|
||||||
return x.toTruncFixed(1);
|
|
||||||
} else {
|
|
||||||
return x.toTruncFixed(0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Format a ratio to a string
|
|
||||||
*/
|
|
||||||
ratioString: function (x) {
|
|
||||||
if (x === -1) {
|
|
||||||
return 'None';
|
|
||||||
}
|
|
||||||
if (x === -2) {
|
|
||||||
return '∞';
|
|
||||||
}
|
|
||||||
return this.percentString(x);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats the a memory size into a human-readable string
|
|
||||||
* @param {Number} bytes the filesize in bytes
|
|
||||||
* @return {String} human-readable string
|
|
||||||
*/
|
|
||||||
mem: function (bytes) {
|
|
||||||
if (bytes < mem_K) {
|
|
||||||
return [bytes, mem_B_str].join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
let convertedSize;
|
|
||||||
let unit;
|
|
||||||
|
|
||||||
if (bytes < Math.pow(mem_K, 2)) {
|
|
||||||
convertedSize = bytes / mem_K;
|
|
||||||
unit = mem_K_str;
|
|
||||||
} else if (bytes < Math.pow(mem_K, 3)) {
|
|
||||||
convertedSize = bytes / Math.pow(mem_K, 2);
|
|
||||||
unit = mem_M_str;
|
|
||||||
} else if (bytes < Math.pow(mem_K, 4)) {
|
|
||||||
convertedSize = bytes / Math.pow(mem_K, 3);
|
|
||||||
unit = mem_G_str;
|
|
||||||
} else {
|
|
||||||
convertedSize = bytes / Math.pow(mem_K, 4);
|
|
||||||
unit = mem_T_str;
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to have at least 3 digits and at least 1 decimal
|
|
||||||
return convertedSize <= 9.995
|
|
||||||
? [convertedSize.toTruncFixed(2), unit].join(' ')
|
|
||||||
: [convertedSize.toTruncFixed(1), unit].join(' ');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats the a disk capacity or file size into a human-readable string
|
|
||||||
* @param {Number} bytes the filesize in bytes
|
|
||||||
* @return {String} human-readable string
|
|
||||||
*/
|
|
||||||
size: function (bytes) {
|
|
||||||
if (bytes < size_K) {
|
|
||||||
return [bytes, size_B_str].join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
let convertedSize;
|
|
||||||
let unit;
|
|
||||||
|
|
||||||
if (bytes < Math.pow(size_K, 2)) {
|
|
||||||
convertedSize = bytes / size_K;
|
|
||||||
unit = size_K_str;
|
|
||||||
} else if (bytes < Math.pow(size_K, 3)) {
|
|
||||||
convertedSize = bytes / Math.pow(size_K, 2);
|
|
||||||
unit = size_M_str;
|
|
||||||
} else if (bytes < Math.pow(size_K, 4)) {
|
|
||||||
convertedSize = bytes / Math.pow(size_K, 3);
|
|
||||||
unit = size_G_str;
|
|
||||||
} else {
|
|
||||||
convertedSize = bytes / Math.pow(size_K, 4);
|
|
||||||
unit = size_T_str;
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to have at least 3 digits and at least 1 decimal
|
|
||||||
return convertedSize <= 9.995
|
|
||||||
? [convertedSize.toTruncFixed(2), unit].join(' ')
|
|
||||||
: [convertedSize.toTruncFixed(1), unit].join(' ');
|
|
||||||
},
|
|
||||||
|
|
||||||
speedBps: function (Bps) {
|
|
||||||
return this.speed(this.toKBps(Bps));
|
|
||||||
},
|
|
||||||
|
|
||||||
toKBps: function (Bps) {
|
|
||||||
return Math.floor(Bps / speed_K);
|
|
||||||
},
|
|
||||||
|
|
||||||
speed: function (KBps) {
|
|
||||||
let speed = KBps;
|
|
||||||
|
|
||||||
if (speed <= 999.95) {
|
|
||||||
// 0 KBps to 999 K
|
|
||||||
return [speed.toTruncFixed(0), speed_K_str].join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
speed /= speed_K;
|
|
||||||
|
|
||||||
if (speed <= 99.995) {
|
|
||||||
// 1 M to 99.99 M
|
|
||||||
return [speed.toTruncFixed(2), speed_M_str].join(' ');
|
|
||||||
}
|
|
||||||
if (speed <= 999.95) {
|
|
||||||
// 100 M to 999.9 M
|
|
||||||
return [speed.toTruncFixed(1), speed_M_str].join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
// insane speeds
|
|
||||||
speed /= speed_K;
|
|
||||||
return [speed.toTruncFixed(2), speed_G_str].join(' ');
|
|
||||||
},
|
|
||||||
|
|
||||||
timeInterval: function (seconds) {
|
|
||||||
const days = Math.floor(seconds / 86400);
|
|
||||||
const hours = Math.floor((seconds % 86400) / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
seconds = Math.floor(seconds % 60);
|
|
||||||
const d = days + ' ' + (days > 1 ? 'days' : 'day');
|
|
||||||
const h = hours + ' ' + (hours > 1 ? 'hours' : 'hour');
|
|
||||||
const m = minutes + ' ' + (minutes > 1 ? 'minutes' : 'minute');
|
|
||||||
const s = seconds + ' ' + (seconds > 1 ? 'seconds' : 'second');
|
|
||||||
|
|
||||||
if (days) {
|
|
||||||
if (days >= 4 || !hours) {
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
return d + ', ' + h;
|
|
||||||
}
|
|
||||||
if (hours) {
|
|
||||||
if (hours >= 4 || !minutes) {
|
|
||||||
return h;
|
|
||||||
}
|
|
||||||
return h + ', ' + m;
|
|
||||||
}
|
|
||||||
if (minutes) {
|
|
||||||
if (minutes >= 4 || !seconds) {
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
return m + ', ' + s;
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
},
|
|
||||||
|
|
||||||
timestamp: function (seconds) {
|
|
||||||
if (!seconds) {
|
|
||||||
return 'N/A';
|
|
||||||
}
|
|
||||||
|
|
||||||
const myDate = new Date(seconds * 1000);
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
let date = '';
|
|
||||||
let time = '';
|
|
||||||
|
|
||||||
const sameYear = now.getFullYear() === myDate.getFullYear();
|
|
||||||
const sameMonth = now.getMonth() === myDate.getMonth();
|
|
||||||
|
|
||||||
const dateDiff = now.getDate() - myDate.getDate();
|
|
||||||
if (sameYear && sameMonth && Math.abs(dateDiff) <= 1) {
|
|
||||||
if (dateDiff === 0) {
|
|
||||||
date = 'Today';
|
|
||||||
} else if (dateDiff === 1) {
|
|
||||||
date = 'Yesterday';
|
|
||||||
} else {
|
|
||||||
date = 'Tomorrow';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
date = myDate.toDateString();
|
|
||||||
}
|
|
||||||
|
|
||||||
let hours = myDate.getHours();
|
|
||||||
let period = 'AM';
|
|
||||||
if (hours > 12) {
|
|
||||||
hours = hours - 12;
|
|
||||||
period = 'PM';
|
|
||||||
}
|
|
||||||
if (hours === 0) {
|
|
||||||
hours = 12;
|
|
||||||
}
|
|
||||||
if (hours < 10) {
|
|
||||||
hours = '0' + hours;
|
|
||||||
}
|
|
||||||
let minutes = myDate.getMinutes();
|
|
||||||
if (minutes < 10) {
|
|
||||||
minutes = '0' + minutes;
|
|
||||||
}
|
|
||||||
seconds = myDate.getSeconds();
|
|
||||||
if (seconds < 10) {
|
|
||||||
seconds = '0' + seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
time = [hours, minutes, seconds].join(':');
|
|
||||||
|
|
||||||
return [date, time, period].join(' ');
|
|
||||||
},
|
|
||||||
|
|
||||||
ngettext: function (msgid, msgid_plural, n) {
|
|
||||||
// TODO(i18n): http://doc.qt.digia.com/4.6/i18n-plural-rules.html
|
|
||||||
return n === 1 ? msgid : msgid_plural;
|
|
||||||
},
|
|
||||||
|
|
||||||
countString: function (msgid, msgid_plural, n) {
|
|
||||||
return [n.toStringWithCommas(), this.ngettext(msgid, msgid_plural, n)].join(' ');
|
|
||||||
},
|
|
||||||
|
|
||||||
peerStatus: function (flagStr) {
|
|
||||||
const formattedFlags = [];
|
|
||||||
for (var i = 0, flag; (flag = flagStr[i]); ++i) {
|
|
||||||
let explanation = null;
|
|
||||||
switch (flag) {
|
|
||||||
case 'O':
|
|
||||||
explanation = 'Optimistic unchoke';
|
|
||||||
break;
|
|
||||||
case 'D':
|
|
||||||
explanation = 'Downloading from this peer';
|
|
||||||
break;
|
|
||||||
case 'd':
|
|
||||||
explanation = "We would download from this peer if they'd let us";
|
|
||||||
break;
|
|
||||||
case 'U':
|
|
||||||
explanation = 'Uploading to peer';
|
|
||||||
break;
|
|
||||||
case 'u':
|
|
||||||
explanation = "We would upload to this peer if they'd ask";
|
|
||||||
break;
|
|
||||||
case 'K':
|
|
||||||
explanation = "Peer has unchoked us, but we're not interested";
|
|
||||||
break;
|
|
||||||
case '?':
|
|
||||||
explanation = "We unchoked this peer, but they're not interested";
|
|
||||||
break;
|
|
||||||
case 'E':
|
|
||||||
explanation = 'Encrypted Connection';
|
|
||||||
break;
|
|
||||||
case 'H':
|
|
||||||
explanation = 'Peer was discovered through Distributed Hash Table (DHT)';
|
|
||||||
break;
|
|
||||||
case 'X':
|
|
||||||
explanation = 'Peer was discovered through Peer Exchange (PEX)';
|
|
||||||
break;
|
|
||||||
case 'I':
|
|
||||||
explanation = 'Peer is an incoming connection';
|
|
||||||
break;
|
|
||||||
case 'T':
|
|
||||||
explanation = 'Peer is connected via uTP';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!explanation) {
|
|
||||||
formattedFlags.push(flag);
|
|
||||||
} else {
|
|
||||||
formattedFlags.push(
|
|
||||||
'<span title="' + flag + ': ' + explanation + '">' + flag + '</span>'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return formattedFlags.join('');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})();
|
|
|
@ -1,958 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright © Charles Kerr, Dave Perrett, Malcolm Jarvis and Bruno Bierbaumer
|
|
||||||
*
|
|
||||||
* This file is licensed under the GPLv2.
|
|
||||||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Inspector(controller) {
|
|
||||||
var data = {
|
|
||||||
controller: null,
|
|
||||||
elements: {},
|
|
||||||
torrents: [],
|
|
||||||
},
|
|
||||||
needsExtraInfo = function (torrents) {
|
|
||||||
return torrents.some((tor) => !tor.hasExtraInfo());
|
|
||||||
},
|
|
||||||
refreshTorrents = function (callback) {
|
|
||||||
let fields,
|
|
||||||
ids = $.map(data.torrents.slice(0), function (t) {
|
|
||||||
return t.getId();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ids && ids.length) {
|
|
||||||
fields = ['id'].concat(Torrent.Fields.StatsExtra);
|
|
||||||
|
|
||||||
if (needsExtraInfo(data.torrents)) {
|
|
||||||
$.merge(fields, Torrent.Fields.InfoExtra);
|
|
||||||
}
|
|
||||||
|
|
||||||
data.controller.updateTorrents(ids, fields, callback);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTabClicked = function (ev) {
|
|
||||||
const tab = ev.currentTarget;
|
|
||||||
|
|
||||||
if (isMobileDevice) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
// select this tab and deselect the others
|
|
||||||
$(tab).addClass('selected').siblings().removeClass('selected');
|
|
||||||
|
|
||||||
// show this tab and hide the others
|
|
||||||
$('#' + tab.id.replace('tab', 'page'))
|
|
||||||
.show()
|
|
||||||
.siblings('.inspector-page')
|
|
||||||
.hide();
|
|
||||||
|
|
||||||
updateInspector();
|
|
||||||
},
|
|
||||||
updateInspector = function () {
|
|
||||||
let e = data.elements,
|
|
||||||
torrents = data.torrents,
|
|
||||||
name;
|
|
||||||
|
|
||||||
// update the name, which is shown on all the pages
|
|
||||||
if (!torrents || !torrents.length) {
|
|
||||||
name = 'No Selection';
|
|
||||||
} else if (torrents.length === 1) {
|
|
||||||
name = torrents[0].getName();
|
|
||||||
} else {
|
|
||||||
name = '' + torrents.length + ' Transfers Selected';
|
|
||||||
}
|
|
||||||
setTextContent(e.name_lb, name || na);
|
|
||||||
|
|
||||||
// update the visible page
|
|
||||||
if ($(e.info_page).is(':visible')) {
|
|
||||||
updateInfoPage();
|
|
||||||
} else if ($(e.peers_page).is(':visible')) {
|
|
||||||
updatePeersPage();
|
|
||||||
} else if ($(e.trackers_page).is(':visible')) {
|
|
||||||
updateTrackersPage();
|
|
||||||
} else if ($(e.files_page).is(':visible')) {
|
|
||||||
updateFilesPage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/****
|
|
||||||
***** GENERAL INFO PAGE
|
|
||||||
****/
|
|
||||||
|
|
||||||
updateInfoPage = function () {
|
|
||||||
let torrents = data.torrents,
|
|
||||||
e = data.elements,
|
|
||||||
fmt = Transmission.fmt,
|
|
||||||
none = 'None',
|
|
||||||
mixed = 'Mixed',
|
|
||||||
unknown = 'Unknown',
|
|
||||||
isMixed,
|
|
||||||
allPaused,
|
|
||||||
allFinished,
|
|
||||||
str,
|
|
||||||
baseline,
|
|
||||||
it,
|
|
||||||
i,
|
|
||||||
t,
|
|
||||||
sizeWhenDone = 0,
|
|
||||||
leftUntilDone = 0,
|
|
||||||
available = 0,
|
|
||||||
haveVerified = 0,
|
|
||||||
haveUnverified = 0,
|
|
||||||
verifiedPieces = 0,
|
|
||||||
stateString,
|
|
||||||
latest,
|
|
||||||
pieces,
|
|
||||||
size,
|
|
||||||
pieceSize,
|
|
||||||
creator,
|
|
||||||
mixed_creator,
|
|
||||||
date,
|
|
||||||
mixed_date,
|
|
||||||
v,
|
|
||||||
u,
|
|
||||||
f,
|
|
||||||
d,
|
|
||||||
now = Date.now();
|
|
||||||
|
|
||||||
//
|
|
||||||
// state_lb
|
|
||||||
//
|
|
||||||
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
str = none;
|
|
||||||
} else {
|
|
||||||
isMixed = false;
|
|
||||||
allPaused = true;
|
|
||||||
allFinished = true;
|
|
||||||
|
|
||||||
baseline = torrents[0].getStatus();
|
|
||||||
for (i = 0; (t = torrents[i]); ++i) {
|
|
||||||
it = t.getStatus();
|
|
||||||
if (it != baseline) {
|
|
||||||
isMixed = true;
|
|
||||||
}
|
|
||||||
if (!t.isStopped()) {
|
|
||||||
allPaused = allFinished = false;
|
|
||||||
}
|
|
||||||
if (!t.isFinished()) {
|
|
||||||
allFinished = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isMixed) {
|
|
||||||
str = mixed;
|
|
||||||
} else if (allFinished) {
|
|
||||||
str = 'Finished';
|
|
||||||
} else if (allPaused) {
|
|
||||||
str = 'Paused';
|
|
||||||
} else {
|
|
||||||
str = torrents[0].getStateString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTextContent(e.state_lb, str);
|
|
||||||
stateString = str;
|
|
||||||
|
|
||||||
//
|
|
||||||
// have_lb
|
|
||||||
//
|
|
||||||
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
str = none;
|
|
||||||
} else {
|
|
||||||
baseline = torrents[0].getStatus();
|
|
||||||
for (i = 0; (t = torrents[i]); ++i) {
|
|
||||||
if (!t.needsMetaData()) {
|
|
||||||
haveUnverified += t.getHaveUnchecked();
|
|
||||||
v = t.getHaveValid();
|
|
||||||
haveVerified += v;
|
|
||||||
if (t.getPieceSize()) {
|
|
||||||
verifiedPieces += v / t.getPieceSize();
|
|
||||||
}
|
|
||||||
sizeWhenDone += t.getSizeWhenDone();
|
|
||||||
leftUntilDone += t.getLeftUntilDone();
|
|
||||||
available += t.getHave() + t.getDesiredAvailable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = 100.0 * (sizeWhenDone ? (sizeWhenDone - leftUntilDone) / sizeWhenDone : 1);
|
|
||||||
str = fmt.percentString(d);
|
|
||||||
|
|
||||||
if (!haveUnverified && !leftUntilDone) {
|
|
||||||
str = fmt.size(haveVerified) + ' (100%)';
|
|
||||||
} else if (!haveUnverified) {
|
|
||||||
str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str + '%)';
|
|
||||||
} else {
|
|
||||||
str =
|
|
||||||
fmt.size(haveVerified) +
|
|
||||||
' of ' +
|
|
||||||
fmt.size(sizeWhenDone) +
|
|
||||||
' (' +
|
|
||||||
str +
|
|
||||||
'%), ' +
|
|
||||||
fmt.size(haveUnverified) +
|
|
||||||
' Unverified';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTextContent(e.have_lb, str);
|
|
||||||
|
|
||||||
//
|
|
||||||
// availability_lb
|
|
||||||
//
|
|
||||||
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
str = none;
|
|
||||||
} else if (sizeWhenDone == 0) {
|
|
||||||
str = none;
|
|
||||||
} else {
|
|
||||||
str = '' + fmt.percentString((100.0 * available) / sizeWhenDone) + '%';
|
|
||||||
}
|
|
||||||
setTextContent(e.availability_lb, str);
|
|
||||||
|
|
||||||
//
|
|
||||||
// downloaded_lb
|
|
||||||
//
|
|
||||||
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
str = none;
|
|
||||||
} else {
|
|
||||||
d = f = 0;
|
|
||||||
for (i = 0; (t = torrents[i]); ++i) {
|
|
||||||
d += t.getDownloadedEver();
|
|
||||||
f += t.getFailedEver();
|
|
||||||
}
|
|
||||||
if (f) {
|
|
||||||
str = fmt.size(d) + ' (' + fmt.size(f) + ' corrupt)';
|
|
||||||
} else {
|
|
||||||
str = fmt.size(d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTextContent(e.downloaded_lb, str);
|
|
||||||
|
|
||||||
//
|
|
||||||
// uploaded_lb
|
|
||||||
//
|
|
||||||
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
str = none;
|
|
||||||
} else {
|
|
||||||
d = u = 0;
|
|
||||||
if (torrents.length == 1) {
|
|
||||||
d = torrents[0].getDownloadedEver();
|
|
||||||
u = torrents[0].getUploadedEver();
|
|
||||||
|
|
||||||
if (d == 0) {
|
|
||||||
d = torrents[0].getHaveValid();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (i = 0; (t = torrents[i]); ++i) {
|
|
||||||
d += t.getDownloadedEver();
|
|
||||||
u += t.getUploadedEver();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
str = fmt.size(u) + ' (Ratio: ' + fmt.ratioString(Math.ratio(u, d)) + ')';
|
|
||||||
}
|
|
||||||
setTextContent(e.uploaded_lb, str);
|
|
||||||
|
|
||||||
//
|
|
||||||
// running time
|
|
||||||
//
|
|
||||||
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
str = none;
|
|
||||||
} else {
|
|
||||||
allPaused = true;
|
|
||||||
baseline = torrents[0].getStartDate();
|
|
||||||
for (i = 0; (t = torrents[i]); ++i) {
|
|
||||||
if (baseline != t.getStartDate()) {
|
|
||||||
baseline = 0;
|
|
||||||
}
|
|
||||||
if (!t.isStopped()) {
|
|
||||||
allPaused = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (allPaused) {
|
|
||||||
str = stateString; // paused || finished}
|
|
||||||
} else if (!baseline) {
|
|
||||||
str = mixed;
|
|
||||||
} else {
|
|
||||||
str = fmt.timeInterval(now / 1000 - baseline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTextContent(e.running_time_lb, str);
|
|
||||||
|
|
||||||
//
|
|
||||||
// remaining time
|
|
||||||
//
|
|
||||||
|
|
||||||
str = '';
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
str = none;
|
|
||||||
} else {
|
|
||||||
baseline = torrents[0].getETA();
|
|
||||||
for (i = 0; (t = torrents[i]); ++i) {
|
|
||||||
if (baseline != t.getETA()) {
|
|
||||||
str = mixed;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!str.length) {
|
|
||||||
if (baseline < 0) {
|
|
||||||
str = unknown;
|
|
||||||
} else {
|
|
||||||
str = fmt.timeInterval(baseline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTextContent(e.remaining_time_lb, str);
|
|
||||||
|
|
||||||
//
|
|
||||||
// last activity
|
|
||||||
//
|
|
||||||
|
|
||||||
latest = -1;
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
str = none;
|
|
||||||
} else {
|
|
||||||
for (i = 0; (t = torrents[i]); ++i) {
|
|
||||||
d = t.getLastActivity();
|
|
||||||
if (latest < d) {
|
|
||||||
latest = d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d = now / 1000 - latest; // seconds since last activity
|
|
||||||
if (d < 0) {
|
|
||||||
str = none;
|
|
||||||
} else if (d < 5) {
|
|
||||||
str = 'Active now';
|
|
||||||
} else {
|
|
||||||
str = fmt.timeInterval(d) + ' ago';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTextContent(e.last_activity_lb, str);
|
|
||||||
|
|
||||||
//
|
|
||||||
// error
|
|
||||||
//
|
|
||||||
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
str = none;
|
|
||||||
} else {
|
|
||||||
str = torrents[0].getErrorString();
|
|
||||||
for (i = 0; (t = torrents[i]); ++i) {
|
|
||||||
if (str != t.getErrorString()) {
|
|
||||||
str = mixed;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTextContent(e.error_lb, str || none);
|
|
||||||
|
|
||||||
//
|
|
||||||
// size
|
|
||||||
//
|
|
||||||
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
{
|
|
||||||
str = none;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pieces = 0;
|
|
||||||
size = 0;
|
|
||||||
pieceSize = torrents[0].getPieceSize();
|
|
||||||
for (i = 0; (t = torrents[i]); ++i) {
|
|
||||||
pieces += t.getPieceCount();
|
|
||||||
size += t.getTotalSize();
|
|
||||||
if (pieceSize != t.getPieceSize()) {
|
|
||||||
pieceSize = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!size) {
|
|
||||||
str = none;
|
|
||||||
} else if (pieceSize > 0) {
|
|
||||||
str =
|
|
||||||
fmt.size(size) +
|
|
||||||
' (' +
|
|
||||||
pieces.toStringWithCommas() +
|
|
||||||
' pieces @ ' +
|
|
||||||
fmt.mem(pieceSize) +
|
|
||||||
')';
|
|
||||||
} else {
|
|
||||||
str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTextContent(e.size_lb, str);
|
|
||||||
|
|
||||||
//
|
|
||||||
// hash
|
|
||||||
//
|
|
||||||
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
str = none;
|
|
||||||
} else {
|
|
||||||
str = torrents[0].getHashString();
|
|
||||||
for (i = 0; (t = torrents[i]); ++i) {
|
|
||||||
if (str != t.getHashString()) {
|
|
||||||
str = mixed;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTextContent(e.hash_lb, str);
|
|
||||||
|
|
||||||
//
|
|
||||||
// privacy
|
|
||||||
//
|
|
||||||
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
str = none;
|
|
||||||
} else {
|
|
||||||
baseline = torrents[0].getPrivateFlag();
|
|
||||||
str = baseline ? 'Private to this tracker -- DHT and PEX disabled' : 'Public torrent';
|
|
||||||
for (i = 0; (t = torrents[i]); ++i) {
|
|
||||||
if (baseline != t.getPrivateFlag()) {
|
|
||||||
str = mixed;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTextContent(e.privacy_lb, str);
|
|
||||||
|
|
||||||
//
|
|
||||||
// comment
|
|
||||||
//
|
|
||||||
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
str = none;
|
|
||||||
} else {
|
|
||||||
str = torrents[0].getComment();
|
|
||||||
for (i = 0; (t = torrents[i]); ++i) {
|
|
||||||
if (str != t.getComment()) {
|
|
||||||
str = mixed;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!str) {
|
|
||||||
str = none;
|
|
||||||
}
|
|
||||||
if (str.startsWith('https://') || str.startsWith('http://')) {
|
|
||||||
str = encodeURI(str);
|
|
||||||
setInnerHTML(e.comment_lb, '<a href="' + str + '" target="_blank" >' + str + '</a>');
|
|
||||||
} else {
|
|
||||||
setTextContent(e.comment_lb, str);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// origin
|
|
||||||
//
|
|
||||||
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
str = none;
|
|
||||||
} else {
|
|
||||||
mixed_creator = false;
|
|
||||||
mixed_date = false;
|
|
||||||
creator = torrents[0].getCreator();
|
|
||||||
date = torrents[0].getDateCreated();
|
|
||||||
for (i = 0; (t = torrents[i]); ++i) {
|
|
||||||
if (creator != t.getCreator()) {
|
|
||||||
mixed_creator = true;
|
|
||||||
}
|
|
||||||
if (date != t.getDateCreated()) {
|
|
||||||
mixed_date = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const empty_creator = !creator || !creator.length;
|
|
||||||
const empty_date = !date;
|
|
||||||
if (mixed_creator || mixed_date) {
|
|
||||||
str = mixed;
|
|
||||||
} else if (empty_creator && empty_date) {
|
|
||||||
str = unknown;
|
|
||||||
} else if (empty_date && !empty_creator) {
|
|
||||||
str = 'Created by ' + creator;
|
|
||||||
} else if (empty_creator && !empty_date) {
|
|
||||||
str = 'Created on ' + new Date(date * 1000).toDateString();
|
|
||||||
} else {
|
|
||||||
str = 'Created by ' + creator + ' on ' + new Date(date * 1000).toDateString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTextContent(e.origin_lb, str);
|
|
||||||
|
|
||||||
//
|
|
||||||
// foldername
|
|
||||||
//
|
|
||||||
|
|
||||||
if (torrents.length < 1) {
|
|
||||||
str = none;
|
|
||||||
} else {
|
|
||||||
str = torrents[0].getDownloadDir();
|
|
||||||
for (i = 0; (t = torrents[i]); ++i) {
|
|
||||||
if (str != t.getDownloadDir()) {
|
|
||||||
str = mixed;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTextContent(e.foldername_lb, str);
|
|
||||||
},
|
|
||||||
/****
|
|
||||||
***** FILES PAGE
|
|
||||||
****/
|
|
||||||
|
|
||||||
changeFileCommand = function (fileIndices, command) {
|
|
||||||
const torrentId = data.file_torrent.getId();
|
|
||||||
data.controller.changeFileCommand(torrentId, fileIndices, command);
|
|
||||||
},
|
|
||||||
onFileWantedToggled = function (ev, fileIndices, want) {
|
|
||||||
changeFileCommand(fileIndices, want ? 'files-wanted' : 'files-unwanted');
|
|
||||||
},
|
|
||||||
onFilePriorityToggled = function (ev, fileIndices, priority) {
|
|
||||||
let command;
|
|
||||||
switch (priority) {
|
|
||||||
case -1:
|
|
||||||
command = 'priority-low';
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
command = 'priority-high';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
command = 'priority-normal';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
changeFileCommand(fileIndices, command);
|
|
||||||
},
|
|
||||||
onNameClicked = function (ev, fileRow, fileIndices) {
|
|
||||||
$(fileRow.getElement()).siblings().slideToggle();
|
|
||||||
},
|
|
||||||
clearFileList = function () {
|
|
||||||
$(data.elements.file_list).empty();
|
|
||||||
delete data.file_torrent;
|
|
||||||
delete data.file_torrent_n;
|
|
||||||
delete data.file_rows;
|
|
||||||
},
|
|
||||||
createFileTreeModel = function (tor) {
|
|
||||||
let i,
|
|
||||||
j,
|
|
||||||
n,
|
|
||||||
name,
|
|
||||||
tokens,
|
|
||||||
walk,
|
|
||||||
token,
|
|
||||||
sub,
|
|
||||||
leaves = [],
|
|
||||||
tree = {
|
|
||||||
children: {},
|
|
||||||
file_indices: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
n = tor.getFileCount();
|
|
||||||
for (i = 0; i < n; ++i) {
|
|
||||||
name = tor.getFile(i).name;
|
|
||||||
tokens = name.split('/');
|
|
||||||
walk = tree;
|
|
||||||
for (j = 0; j < tokens.length; ++j) {
|
|
||||||
token = tokens[j];
|
|
||||||
sub = walk.children[token];
|
|
||||||
if (!sub) {
|
|
||||||
walk.children[token] = sub = {
|
|
||||||
name: token,
|
|
||||||
parent: walk,
|
|
||||||
children: {},
|
|
||||||
file_indices: [],
|
|
||||||
depth: j,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
walk = sub;
|
|
||||||
}
|
|
||||||
walk.file_index = i;
|
|
||||||
delete walk.children;
|
|
||||||
leaves.push(walk);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i < leaves.length; ++i) {
|
|
||||||
walk = leaves[i];
|
|
||||||
j = walk.file_index;
|
|
||||||
do {
|
|
||||||
walk.file_indices.push(j);
|
|
||||||
walk = walk.parent;
|
|
||||||
} while (walk);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tree;
|
|
||||||
},
|
|
||||||
addNodeToView = function (tor, parent, sub, i) {
|
|
||||||
let row;
|
|
||||||
row = new FileRow(tor, sub.depth, sub.name, sub.file_indices, i % 2);
|
|
||||||
data.file_rows.push(row);
|
|
||||||
parent.appendChild(row.getElement());
|
|
||||||
$(row).bind('wantedToggled', onFileWantedToggled);
|
|
||||||
$(row).bind('priorityToggled', onFilePriorityToggled);
|
|
||||||
$(row).bind('nameClicked', onNameClicked);
|
|
||||||
},
|
|
||||||
addSubtreeToView = function (tor, parent, sub, i) {
|
|
||||||
let key, div;
|
|
||||||
div = document.createElement('div');
|
|
||||||
if (sub.parent) {
|
|
||||||
addNodeToView(tor, div, sub, i++);
|
|
||||||
}
|
|
||||||
if (sub.children) {
|
|
||||||
for (key in sub.children) {
|
|
||||||
i = addSubtreeToView(tor, div, sub.children[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parent.appendChild(div);
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
updateFilesPage = function () {
|
|
||||||
let i,
|
|
||||||
n,
|
|
||||||
tor,
|
|
||||||
fragment,
|
|
||||||
tree,
|
|
||||||
file_list = data.elements.file_list,
|
|
||||||
torrents = data.torrents;
|
|
||||||
|
|
||||||
// only show one torrent at a time
|
|
||||||
if (torrents.length !== 1) {
|
|
||||||
clearFileList();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tor = torrents[0];
|
|
||||||
n = tor ? tor.getFileCount() : 0;
|
|
||||||
if (tor != data.file_torrent || n != data.file_torrent_n) {
|
|
||||||
// rebuild the file list...
|
|
||||||
clearFileList();
|
|
||||||
data.file_torrent = tor;
|
|
||||||
data.file_torrent_n = n;
|
|
||||||
data.file_rows = [];
|
|
||||||
fragment = document.createDocumentFragment();
|
|
||||||
tree = createFileTreeModel(tor);
|
|
||||||
addSubtreeToView(tor, fragment, tree, 0);
|
|
||||||
file_list.appendChild(fragment);
|
|
||||||
} else {
|
|
||||||
// ...refresh the already-existing file list
|
|
||||||
for (i = 0, n = data.file_rows.length; i < n; ++i) {
|
|
||||||
data.file_rows[i].refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/****
|
|
||||||
***** PEERS PAGE
|
|
||||||
****/
|
|
||||||
|
|
||||||
updatePeersPage = function () {
|
|
||||||
let i,
|
|
||||||
k,
|
|
||||||
tor,
|
|
||||||
peers,
|
|
||||||
peer,
|
|
||||||
parity,
|
|
||||||
html = [],
|
|
||||||
fmt = Transmission.fmt,
|
|
||||||
peers_list = data.elements.peers_list,
|
|
||||||
torrents = data.torrents;
|
|
||||||
|
|
||||||
for (k = 0; (tor = torrents[k]); ++k) {
|
|
||||||
peers = tor.getPeers();
|
|
||||||
html.push('<div class="inspector_group">');
|
|
||||||
if (torrents.length > 1) {
|
|
||||||
html.push('<div class="inspector_torrent_label">', sanitizeText(tor.getName()), '</div>');
|
|
||||||
}
|
|
||||||
if (!peers || !peers.length) {
|
|
||||||
html.push('<br></div>'); // firefox won't paint the top border if the div is empty
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
html.push(
|
|
||||||
'<table class="peer_list">',
|
|
||||||
'<tr class="inspector_peer_entry even">',
|
|
||||||
'<th class="encryptedCol"></th>',
|
|
||||||
'<th class="upCol">Up</th>',
|
|
||||||
'<th class="downCol">Down</th>',
|
|
||||||
'<th class="percentCol">%</th>',
|
|
||||||
'<th class="statusCol">Status</th>',
|
|
||||||
'<th class="addressCol">Address</th>',
|
|
||||||
'<th class="clientCol">Client</th>',
|
|
||||||
'</tr>'
|
|
||||||
);
|
|
||||||
for (i = 0; (peer = peers[i]); ++i) {
|
|
||||||
parity = i % 2 ? 'odd' : 'even';
|
|
||||||
html.push(
|
|
||||||
'<tr class="inspector_peer_entry ',
|
|
||||||
parity,
|
|
||||||
'">',
|
|
||||||
'<td>',
|
|
||||||
peer.isEncrypted
|
|
||||||
? '<div class="encrypted-peer-cell" title="Encrypted Connection">'
|
|
||||||
: '<div class="unencrypted-peer-cell">',
|
|
||||||
'</div>',
|
|
||||||
'</td>',
|
|
||||||
'<td>',
|
|
||||||
peer.rateToPeer ? fmt.speedBps(peer.rateToPeer) : '',
|
|
||||||
'</td>',
|
|
||||||
'<td>',
|
|
||||||
peer.rateToClient ? fmt.speedBps(peer.rateToClient) : '',
|
|
||||||
'</td>',
|
|
||||||
'<td class="percentCol">',
|
|
||||||
Math.floor(peer.progress * 100),
|
|
||||||
'%',
|
|
||||||
'</td>',
|
|
||||||
'<td>',
|
|
||||||
fmt.peerStatus(peer.flagStr),
|
|
||||||
'</td>',
|
|
||||||
'<td>',
|
|
||||||
sanitizeText(peer.address),
|
|
||||||
'</td>',
|
|
||||||
'<td class="clientCol">',
|
|
||||||
sanitizeText(peer.clientName),
|
|
||||||
'</td>',
|
|
||||||
'</tr>'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
html.push('</table></div>');
|
|
||||||
}
|
|
||||||
|
|
||||||
setInnerHTML(peers_list, html.join(''));
|
|
||||||
},
|
|
||||||
/****
|
|
||||||
***** TRACKERS PAGE
|
|
||||||
****/
|
|
||||||
|
|
||||||
getAnnounceState = function (tracker) {
|
|
||||||
let timeUntilAnnounce,
|
|
||||||
s = '';
|
|
||||||
switch (tracker.announceState) {
|
|
||||||
case Torrent._TrackerActive:
|
|
||||||
s = 'Announce in progress';
|
|
||||||
break;
|
|
||||||
case Torrent._TrackerWaiting:
|
|
||||||
timeUntilAnnounce = tracker.nextAnnounceTime - new Date().getTime() / 1000;
|
|
||||||
if (timeUntilAnnounce < 0) {
|
|
||||||
timeUntilAnnounce = 0;
|
|
||||||
}
|
|
||||||
s = 'Next announce in ' + Transmission.fmt.timeInterval(timeUntilAnnounce);
|
|
||||||
break;
|
|
||||||
case Torrent._TrackerQueued:
|
|
||||||
s = 'Announce is queued';
|
|
||||||
break;
|
|
||||||
case Torrent._TrackerInactive:
|
|
||||||
s = tracker.isBackup ? 'Tracker will be used as a backup' : 'Announce not scheduled';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
s = 'unknown announce state: ' + tracker.announceState;
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
},
|
|
||||||
lastAnnounceStatus = function (tracker) {
|
|
||||||
let lastAnnounceLabel = 'Last Announce',
|
|
||||||
lastAnnounce = ['N/A'],
|
|
||||||
lastAnnounceTime;
|
|
||||||
|
|
||||||
if (tracker.hasAnnounced) {
|
|
||||||
lastAnnounceTime = Transmission.fmt.timestamp(tracker.lastAnnounceTime);
|
|
||||||
if (tracker.lastAnnounceSucceeded) {
|
|
||||||
lastAnnounce = [
|
|
||||||
lastAnnounceTime,
|
|
||||||
' (got ',
|
|
||||||
Transmission.fmt.countString('peer', 'peers', tracker.lastAnnouncePeerCount),
|
|
||||||
')',
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
lastAnnounceLabel = 'Announce error';
|
|
||||||
lastAnnounce = [
|
|
||||||
tracker.lastAnnounceResult ? tracker.lastAnnounceResult + ' - ' : '',
|
|
||||||
lastAnnounceTime,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
label: lastAnnounceLabel,
|
|
||||||
value: lastAnnounce.join(''),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
lastScrapeStatus = function (tracker) {
|
|
||||||
let lastScrapeLabel = 'Last Scrape',
|
|
||||||
lastScrape = 'N/A',
|
|
||||||
lastScrapeTime;
|
|
||||||
|
|
||||||
if (tracker.hasScraped) {
|
|
||||||
lastScrapeTime = Transmission.fmt.timestamp(tracker.lastScrapeTime);
|
|
||||||
if (tracker.lastScrapeSucceeded) {
|
|
||||||
lastScrape = lastScrapeTime;
|
|
||||||
} else {
|
|
||||||
lastScrapeLabel = 'Scrape error';
|
|
||||||
lastScrape =
|
|
||||||
(tracker.lastScrapeResult ? tracker.lastScrapeResult + ' - ' : '') + lastScrapeTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
label: lastScrapeLabel,
|
|
||||||
value: lastScrape,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
updateTrackersPage = function () {
|
|
||||||
let i,
|
|
||||||
j,
|
|
||||||
tier,
|
|
||||||
tracker,
|
|
||||||
trackers,
|
|
||||||
tor,
|
|
||||||
html,
|
|
||||||
parity,
|
|
||||||
lastAnnounceStatusHash,
|
|
||||||
announceState,
|
|
||||||
lastScrapeStatusHash,
|
|
||||||
na = 'N/A',
|
|
||||||
trackers_list = data.elements.trackers_list,
|
|
||||||
torrents = data.torrents;
|
|
||||||
|
|
||||||
// By building up the HTML as as string, then have the browser
|
|
||||||
// turn this into a DOM tree, this is a fast operation.
|
|
||||||
html = [];
|
|
||||||
for (i = 0; (tor = torrents[i]); ++i) {
|
|
||||||
html.push('<div class="inspector_group">');
|
|
||||||
|
|
||||||
if (torrents.length > 1) {
|
|
||||||
html.push('<div class="inspector_torrent_label">', sanitizeText(tor.getName()), '</div>');
|
|
||||||
}
|
|
||||||
|
|
||||||
tier = -1;
|
|
||||||
trackers = tor.getTrackers();
|
|
||||||
for (j = 0; (tracker = trackers[j]); ++j) {
|
|
||||||
if (tier != tracker.tier) {
|
|
||||||
if (tier !== -1) {
|
|
||||||
// close previous tier
|
|
||||||
html.push('</ul></div>');
|
|
||||||
}
|
|
||||||
|
|
||||||
tier = tracker.tier;
|
|
||||||
|
|
||||||
html.push(
|
|
||||||
'<div class="inspector_group_label">',
|
|
||||||
'Tier ',
|
|
||||||
tier + 1,
|
|
||||||
'</div>',
|
|
||||||
'<ul class="tier_list">'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display construction
|
|
||||||
lastAnnounceStatusHash = lastAnnounceStatus(tracker);
|
|
||||||
announceState = getAnnounceState(tracker);
|
|
||||||
lastScrapeStatusHash = lastScrapeStatus(tracker);
|
|
||||||
parity = j % 2 ? 'odd' : 'even';
|
|
||||||
html.push(
|
|
||||||
'<li class="inspector_tracker_entry ',
|
|
||||||
parity,
|
|
||||||
'"><div class="tracker_host" title="',
|
|
||||||
sanitizeText(tracker.announce),
|
|
||||||
'">',
|
|
||||||
sanitizeText(tracker.host || tracker.announce),
|
|
||||||
'</div>',
|
|
||||||
'<div class="tracker_activity">',
|
|
||||||
'<div>',
|
|
||||||
lastAnnounceStatusHash['label'],
|
|
||||||
': ',
|
|
||||||
sanitizeText(lastAnnounceStatusHash['value']),
|
|
||||||
'</div>',
|
|
||||||
'<div>',
|
|
||||||
announceState,
|
|
||||||
'</div>',
|
|
||||||
'<div>',
|
|
||||||
lastScrapeStatusHash['label'],
|
|
||||||
': ',
|
|
||||||
sanitizeText(lastScrapeStatusHash['value']),
|
|
||||||
'</div>',
|
|
||||||
'</div><table class="tracker_stats">',
|
|
||||||
'<tr><th>Seeders:</th><td>',
|
|
||||||
tracker.seederCount > -1 ? tracker.seederCount : na,
|
|
||||||
'</td></tr>',
|
|
||||||
'<tr><th>Leechers:</th><td>',
|
|
||||||
tracker.leecherCount > -1 ? tracker.leecherCount : na,
|
|
||||||
'</td></tr>',
|
|
||||||
'<tr><th>Downloads:</th><td>',
|
|
||||||
tracker.downloadCount > -1 ? tracker.downloadCount : na,
|
|
||||||
'</td></tr>',
|
|
||||||
'</table></li>'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (tier !== -1) {
|
|
||||||
// close last tier
|
|
||||||
html.push('</ul></div>');
|
|
||||||
}
|
|
||||||
|
|
||||||
html.push('</div>'); // inspector_group
|
|
||||||
}
|
|
||||||
|
|
||||||
setInnerHTML(trackers_list, html.join(''));
|
|
||||||
},
|
|
||||||
initialize = function (controller) {
|
|
||||||
data.controller = controller;
|
|
||||||
|
|
||||||
$('.inspector-tab').click(onTabClicked);
|
|
||||||
|
|
||||||
data.elements.info_page = $('#inspector-page-info')[0];
|
|
||||||
data.elements.files_page = $('#inspector-page-files')[0];
|
|
||||||
data.elements.peers_page = $('#inspector-page-peers')[0];
|
|
||||||
data.elements.trackers_page = $('#inspector-page-trackers')[0];
|
|
||||||
|
|
||||||
data.elements.file_list = $('#inspector_file_list')[0];
|
|
||||||
data.elements.peers_list = $('#inspector_peers_list')[0];
|
|
||||||
data.elements.trackers_list = $('#inspector_trackers_list')[0];
|
|
||||||
|
|
||||||
data.elements.have_lb = $('#inspector-info-have')[0];
|
|
||||||
data.elements.availability_lb = $('#inspector-info-availability')[0];
|
|
||||||
data.elements.downloaded_lb = $('#inspector-info-downloaded')[0];
|
|
||||||
data.elements.uploaded_lb = $('#inspector-info-uploaded')[0];
|
|
||||||
data.elements.state_lb = $('#inspector-info-state')[0];
|
|
||||||
data.elements.running_time_lb = $('#inspector-info-running-time')[0];
|
|
||||||
data.elements.remaining_time_lb = $('#inspector-info-remaining-time')[0];
|
|
||||||
data.elements.last_activity_lb = $('#inspector-info-last-activity')[0];
|
|
||||||
data.elements.error_lb = $('#inspector-info-error')[0];
|
|
||||||
data.elements.size_lb = $('#inspector-info-size')[0];
|
|
||||||
data.elements.foldername_lb = $('#inspector-info-location')[0];
|
|
||||||
data.elements.hash_lb = $('#inspector-info-hash')[0];
|
|
||||||
data.elements.privacy_lb = $('#inspector-info-privacy')[0];
|
|
||||||
data.elements.origin_lb = $('#inspector-info-origin')[0];
|
|
||||||
data.elements.comment_lb = $('#inspector-info-comment')[0];
|
|
||||||
data.elements.name_lb = $('#torrent_inspector_name')[0];
|
|
||||||
|
|
||||||
// force initial 'N/A' updates on all the pages
|
|
||||||
updateInspector();
|
|
||||||
updateInfoPage();
|
|
||||||
updatePeersPage();
|
|
||||||
updateTrackersPage();
|
|
||||||
updateFilesPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
/****
|
|
||||||
***** PUBLIC FUNCTIONS
|
|
||||||
****/
|
|
||||||
|
|
||||||
this.setTorrents = function (torrents) {
|
|
||||||
const d = data;
|
|
||||||
|
|
||||||
// update the inspector when a selected torrent's data changes.
|
|
||||||
$(d.torrents).unbind('dataChanged.inspector');
|
|
||||||
$(torrents).bind('dataChanged.inspector', $.proxy(updateInspector, this));
|
|
||||||
d.torrents = torrents;
|
|
||||||
|
|
||||||
// periodically ask for updates to the inspector's torrents
|
|
||||||
clearTimeout(d.refreshTimeout);
|
|
||||||
|
|
||||||
function callback() {
|
|
||||||
refreshTorrents(rescheduleTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
function rescheduleTimeout() {
|
|
||||||
d.refreshTimeout = setTimeout(callback, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
rescheduleTimeout();
|
|
||||||
refreshTorrents();
|
|
||||||
|
|
||||||
// refresh the inspector's UI
|
|
||||||
updateInspector();
|
|
||||||
};
|
|
||||||
|
|
||||||
initialize(controller);
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
/*
|
|
||||||
* This file Copyright (C) 2015 Mnemosyne LLC
|
|
||||||
*
|
|
||||||
* It may be used under the GNU GPL versions 2 or 3
|
|
||||||
* or any future license endorsed by Mnemosyne LLC.
|
|
||||||
*/
|
|
||||||
|
|
||||||
$.widget('tr.transMenu', $.ui.menu, {
|
|
||||||
options: {
|
|
||||||
open: null,
|
|
||||||
close: null
|
|
||||||
},
|
|
||||||
|
|
||||||
_create: function() {
|
|
||||||
this.selectImpl = this.options.select;
|
|
||||||
this.options.select = $.proxy(this._select, this);
|
|
||||||
this.element.hide();
|
|
||||||
this._superApply(arguments);
|
|
||||||
},
|
|
||||||
|
|
||||||
_select: function(event, ui) {
|
|
||||||
if (ui.item.is("[aria-haspopup='true']"))
|
|
||||||
return;
|
|
||||||
ui.id = ui.item.attr("id");
|
|
||||||
ui.group = ui.item.attr("radio-group");
|
|
||||||
ui.target = $(event.currentTarget);
|
|
||||||
if (this.selectImpl(event, ui) !== false)
|
|
||||||
this.close();
|
|
||||||
},
|
|
||||||
|
|
||||||
open: function(event) {
|
|
||||||
this.element.show();
|
|
||||||
this.element.css({ position: "absolute", left: 4, top: -this.element.height() - 4 });
|
|
||||||
|
|
||||||
$(document).bind("keydown" + this.eventNamespace, $.proxy(function(event) {
|
|
||||||
if (event.which === $.ui.keyCode.ESCAPE)
|
|
||||||
this.close();
|
|
||||||
}, this));
|
|
||||||
$(document).bind("mousedown" + this.eventNamespace + " touchstart" + this.eventNamespace, $.proxy(function(event) {
|
|
||||||
if (!$(event.target).closest(".ui-menu-item").length)
|
|
||||||
this.close();
|
|
||||||
}, this));
|
|
||||||
|
|
||||||
this._trigger("open", event);
|
|
||||||
},
|
|
||||||
|
|
||||||
close: function(event) {
|
|
||||||
$(document).unbind("keydown" + this.eventNamespace);
|
|
||||||
$(document).unbind("mousedown" + this.eventNamespace);
|
|
||||||
$(document).unbind("touchstart" + this.eventNamespace);
|
|
||||||
|
|
||||||
this._close(this.element);
|
|
||||||
this.element.hide();
|
|
||||||
|
|
||||||
this._trigger("close", event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
(function($)
|
|
||||||
{
|
|
||||||
function indicatorClass(type)
|
|
||||||
{
|
|
||||||
return ['ui-icon', 'ui-icon-' + type];
|
|
||||||
}
|
|
||||||
|
|
||||||
function findIndicator(item, type)
|
|
||||||
{
|
|
||||||
return $(item).find('span.' + indicatorClass(type).join('.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function createIndicator(item, type)
|
|
||||||
{
|
|
||||||
$(item).prepend($("<span class='" + indicatorClass(type).join(' ') + "'></span>"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function indicatorType(item)
|
|
||||||
{
|
|
||||||
var group = item.attr('radio-group');
|
|
||||||
return { type: group !== undefined ? 'bullet' : 'check', group: group };
|
|
||||||
}
|
|
||||||
|
|
||||||
$.fn.selectMenuItem = function() {
|
|
||||||
var t = indicatorType(this);
|
|
||||||
if (t.type == 'bullet')
|
|
||||||
this.parent().find('li[radio-group=' + t.group + '] span.' + indicatorClass(t.type).join('.')).remove();
|
|
||||||
if (findIndicator(this, t.type).length == 0)
|
|
||||||
createIndicator(this, t.type);
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.deselectMenuItem = function() {
|
|
||||||
var t = indicatorType(this);
|
|
||||||
return findIndicator(this, t.type).remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.menuItemIsSelected = function() {
|
|
||||||
return findIndicator(this, 'bullet').length > 0 || findIndicator(this, 'check').length > 0;
|
|
||||||
};
|
|
||||||
})(jQuery);
|
|
|
@ -1 +0,0 @@
|
||||||
$.widget("tr.transMenu",$.ui.menu,{options:{open:null,close:null},_create:function(){this.selectImpl=this.options.select,this.options.select=$.proxy(this._select,this),this.element.hide(),this._superApply(arguments)},_select:function(e,t){t.item.is("[aria-haspopup='true']")||(t.id=t.item.attr("id"),t.group=t.item.attr("radio-group"),t.target=$(e.currentTarget),this.selectImpl(e,t)!==!1&&this.close())},open:function(e){this.element.show(),this.element.css({position:"absolute",left:4,top:-this.element.height()-4}),$(document).bind("keydown"+this.eventNamespace,$.proxy(function(e){e.which===$.ui.keyCode.ESCAPE&&this.close()},this)),$(document).bind("mousedown"+this.eventNamespace+" touchstart"+this.eventNamespace,$.proxy(function(e){$(e.target).closest(".ui-menu-item").length||this.close()},this)),this._trigger("open",e)},close:function(e){$(document).unbind("keydown"+this.eventNamespace),$(document).unbind("mousedown"+this.eventNamespace),$(document).unbind("touchstart"+this.eventNamespace),this._close(this.element),this.element.hide(),this._trigger("close",e)}}),function($){function e(e){return["ui-icon","ui-icon-"+e]}function t(t,n){return $(t).find("span."+e(n).join("."))}function n(t,n){$(t).prepend($("<span class='"+e(n).join(" ")+"'></span>"))}function i(e){var t=e.attr("radio-group");return{type:void 0!==t?"bullet":"check",group:t}}$.fn.selectMenuItem=function(){var s=i(this);return"bullet"==s.type&&this.parent().find("li[radio-group="+s.group+"] span."+e(s.type).join(".")).remove(),0==t(this,s.type).length&&n(this,s.type),this},$.fn.deselectMenuItem=function(){var e=i(this);return t(this,e.type).remove()},$.fn.menuItemIsSelected=function(){return t(this,"bullet").length>0||t(this,"check").length>0}}(jQuery);
|
|
|
@ -1,469 +0,0 @@
|
||||||
/*******************************************************************************
|
|
||||||
* jquery.ui-contextmenu.js plugin.
|
|
||||||
*
|
|
||||||
* jQuery plugin that provides a context menu (based on the jQueryUI menu widget).
|
|
||||||
*
|
|
||||||
* @see https://github.com/mar10/jquery-ui-contextmenu
|
|
||||||
*
|
|
||||||
* Copyright (c) 2013-2015, Martin Wendt (http://wwWendt.de). Licensed MIT.
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function( factory ) {
|
|
||||||
"use strict";
|
|
||||||
if ( typeof define === "function" && define.amd ) {
|
|
||||||
// AMD. Register as an anonymous module.
|
|
||||||
define([ "jquery", "jquery-ui/menu" ], factory );
|
|
||||||
} else {
|
|
||||||
// Browser globals
|
|
||||||
factory( jQuery );
|
|
||||||
}
|
|
||||||
}(function( $ ) {
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var supportSelectstart = "onselectstart" in document.createElement("div"),
|
|
||||||
match = $.ui.menu.version.match(/^(\d)\.(\d+)/),
|
|
||||||
uiVersion = {
|
|
||||||
major: parseInt(match[1], 10),
|
|
||||||
minor: parseInt(match[2], 10)
|
|
||||||
},
|
|
||||||
isLTE110 = ( uiVersion.major < 2 && uiVersion.minor < 11 );
|
|
||||||
|
|
||||||
$.widget("moogle.contextmenu", {
|
|
||||||
version: "@VERSION",
|
|
||||||
options: {
|
|
||||||
addClass: "ui-contextmenu", // Add this class to the outer <ul>
|
|
||||||
autoFocus: false, // Set keyboard focus to first entry on open
|
|
||||||
autoTrigger: true, // open menu on browser's `contextmenu` event
|
|
||||||
delegate: null, // selector
|
|
||||||
hide: { effect: "fadeOut", duration: "fast" },
|
|
||||||
ignoreParentSelect: true, // Don't trigger 'select' for sub-menu parents
|
|
||||||
menu: null, // selector or jQuery pointing to <UL>, or a definition hash
|
|
||||||
position: null, // popup positon
|
|
||||||
preventContextMenuForPopup: false, // prevent opening the browser's system
|
|
||||||
// context menu on menu entries
|
|
||||||
preventSelect: false, // disable text selection of target
|
|
||||||
show: { effect: "slideDown", duration: "fast" },
|
|
||||||
taphold: false, // open menu on taphold events (requires external plugins)
|
|
||||||
uiMenuOptions: {}, // Additional options, used when UI Menu is created
|
|
||||||
// Events:
|
|
||||||
beforeOpen: $.noop, // menu about to open; return `false` to prevent opening
|
|
||||||
blur: $.noop, // menu option lost focus
|
|
||||||
close: $.noop, // menu was closed
|
|
||||||
create: $.noop, // menu was initialized
|
|
||||||
createMenu: $.noop, // menu was initialized (original UI Menu)
|
|
||||||
focus: $.noop, // menu option got focus
|
|
||||||
open: $.noop, // menu was opened
|
|
||||||
select: $.noop // menu option was selected; return `false` to prevent closing
|
|
||||||
},
|
|
||||||
/** Constructor */
|
|
||||||
_create: function() {
|
|
||||||
var cssText, eventNames, targetId,
|
|
||||||
opts = this.options;
|
|
||||||
|
|
||||||
this.$headStyle = null;
|
|
||||||
this.$menu = null;
|
|
||||||
this.menuIsTemp = false;
|
|
||||||
this.currentTarget = null;
|
|
||||||
this.previousFocus = null;
|
|
||||||
|
|
||||||
if (opts.preventSelect) {
|
|
||||||
// Create a global style for all potential menu targets
|
|
||||||
// If the contextmenu was bound to `document`, we apply the
|
|
||||||
// selector relative to the <body> tag instead
|
|
||||||
targetId = ($(this.element).is(document) ? $("body")
|
|
||||||
: this.element).uniqueId().attr("id");
|
|
||||||
cssText = "#" + targetId + " " + opts.delegate + " { " +
|
|
||||||
"-webkit-user-select: none; " +
|
|
||||||
"-khtml-user-select: none; " +
|
|
||||||
"-moz-user-select: none; " +
|
|
||||||
"-ms-user-select: none; " +
|
|
||||||
"user-select: none; " +
|
|
||||||
"}";
|
|
||||||
this.$headStyle = $("<style class='moogle-contextmenu-style' />")
|
|
||||||
.prop("type", "text/css")
|
|
||||||
.appendTo("head");
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.$headStyle.html(cssText);
|
|
||||||
} catch ( e ) {
|
|
||||||
// issue #47: fix for IE 6-8
|
|
||||||
this.$headStyle[0].styleSheet.cssText = cssText;
|
|
||||||
}
|
|
||||||
// TODO: the selectstart is not supported by FF?
|
|
||||||
if (supportSelectstart) {
|
|
||||||
this.element.delegate(opts.delegate, "selectstart" + this.eventNamespace,
|
|
||||||
function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._createUiMenu(opts.menu);
|
|
||||||
|
|
||||||
eventNames = "contextmenu" + this.eventNamespace;
|
|
||||||
if (opts.taphold) {
|
|
||||||
eventNames += " taphold" + this.eventNamespace;
|
|
||||||
}
|
|
||||||
this.element.delegate(opts.delegate, eventNames, $.proxy(this._openMenu, this));
|
|
||||||
},
|
|
||||||
/** Destructor, called on $().contextmenu("destroy"). */
|
|
||||||
_destroy: function() {
|
|
||||||
this.element.undelegate(this.eventNamespace);
|
|
||||||
|
|
||||||
this._createUiMenu(null);
|
|
||||||
|
|
||||||
if (this.$headStyle) {
|
|
||||||
this.$headStyle.remove();
|
|
||||||
this.$headStyle = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/** (Re)Create jQuery UI Menu. */
|
|
||||||
_createUiMenu: function(menuDef) {
|
|
||||||
var ct,
|
|
||||||
opts = this.options;
|
|
||||||
|
|
||||||
// Remove temporary <ul> if any
|
|
||||||
if (this.isOpen()) {
|
|
||||||
// #58: 'replaceMenu' in beforeOpen causing select: to lose ui.target
|
|
||||||
ct = this.currentTarget;
|
|
||||||
// close without animation, to force async mode
|
|
||||||
this._closeMenu(true);
|
|
||||||
this.currentTarget = ct;
|
|
||||||
}
|
|
||||||
if (this.menuIsTemp) {
|
|
||||||
this.$menu.remove(); // this will also destroy ui.menu
|
|
||||||
} else if (this.$menu) {
|
|
||||||
this.$menu
|
|
||||||
.menu("destroy")
|
|
||||||
.removeClass(this.options.addClass)
|
|
||||||
.hide();
|
|
||||||
}
|
|
||||||
this.$menu = null;
|
|
||||||
this.menuIsTemp = false;
|
|
||||||
// If a menu definition array was passed, create a hidden <ul>
|
|
||||||
// and generate the structure now
|
|
||||||
if ( !menuDef ) {
|
|
||||||
return;
|
|
||||||
} else if ($.isArray(menuDef)) {
|
|
||||||
this.$menu = $.moogle.contextmenu.createMenuMarkup(menuDef);
|
|
||||||
this.menuIsTemp = true;
|
|
||||||
}else if ( typeof menuDef === "string" ) {
|
|
||||||
this.$menu = $(menuDef);
|
|
||||||
} else {
|
|
||||||
this.$menu = menuDef;
|
|
||||||
}
|
|
||||||
// Create - but hide - the jQuery UI Menu widget
|
|
||||||
this.$menu
|
|
||||||
.hide()
|
|
||||||
.addClass(opts.addClass)
|
|
||||||
// Create a menu instance that delegates events to our widget
|
|
||||||
.menu($.extend(true, {}, opts.uiMenuOptions, {
|
|
||||||
blur: $.proxy(opts.blur, this),
|
|
||||||
create: $.proxy(opts.createMenu, this),
|
|
||||||
focus: $.proxy(opts.focus, this),
|
|
||||||
select: $.proxy(function(event, ui) {
|
|
||||||
// User selected a menu entry
|
|
||||||
var retval,
|
|
||||||
isParent = $.moogle.contextmenu.isMenu(ui.item),
|
|
||||||
actionHandler = ui.item.data("actionHandler");
|
|
||||||
|
|
||||||
ui.cmd = ui.item.attr("data-command");
|
|
||||||
ui.target = $(this.currentTarget);
|
|
||||||
// ignore clicks, if they only open a sub-menu
|
|
||||||
if ( !isParent || !opts.ignoreParentSelect) {
|
|
||||||
retval = this._trigger.call(this, "select", event, ui);
|
|
||||||
if ( actionHandler ) {
|
|
||||||
retval = actionHandler.call(this, event, ui);
|
|
||||||
}
|
|
||||||
if ( retval !== false ) {
|
|
||||||
this._closeMenu.call(this);
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}, this)
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
/** Open popup (called on 'contextmenu' event). */
|
|
||||||
_openMenu: function(event, recursive) {
|
|
||||||
var res, promise,
|
|
||||||
opts = this.options,
|
|
||||||
posOption = opts.position,
|
|
||||||
self = this,
|
|
||||||
manualTrigger = !!event.isTrigger,
|
|
||||||
ui = { menu: this.$menu, target: $(event.target),
|
|
||||||
extraData: event.extraData, originalEvent: event,
|
|
||||||
result: null };
|
|
||||||
|
|
||||||
if ( !opts.autoTrigger && !manualTrigger ) {
|
|
||||||
// ignore browser's `contextmenu` events
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent browser from opening the system context menu
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
this.currentTarget = event.target;
|
|
||||||
|
|
||||||
if ( !recursive ) {
|
|
||||||
res = this._trigger("beforeOpen", event, ui);
|
|
||||||
promise = (ui.result && $.isFunction(ui.result.promise)) ? ui.result : null;
|
|
||||||
ui.result = null;
|
|
||||||
if ( res === false ) {
|
|
||||||
this.currentTarget = null;
|
|
||||||
return false;
|
|
||||||
} else if ( promise ) {
|
|
||||||
// Handler returned a Deferred or Promise. Delay menu open until
|
|
||||||
// the promise is resolved
|
|
||||||
promise.done(function() {
|
|
||||||
self._openMenu(event, true);
|
|
||||||
});
|
|
||||||
this.currentTarget = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ui.menu = this.$menu; // Might have changed in beforeOpen
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register global event handlers that close the dropdown-menu
|
|
||||||
$(document).bind("keydown" + this.eventNamespace, function(event) {
|
|
||||||
if ( event.which === $.ui.keyCode.ESCAPE ) {
|
|
||||||
self._closeMenu();
|
|
||||||
}
|
|
||||||
}).bind("mousedown" + this.eventNamespace + " touchstart" + this.eventNamespace,
|
|
||||||
function(event) {
|
|
||||||
// Close menu when clicked outside menu
|
|
||||||
if ( !$(event.target).closest(".ui-menu-item").length ) {
|
|
||||||
self._closeMenu();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// required for custom positioning (issue #18 and #13).
|
|
||||||
if ($.isFunction(posOption)) {
|
|
||||||
posOption = posOption(event, ui);
|
|
||||||
}
|
|
||||||
posOption = $.extend({
|
|
||||||
my: "left top",
|
|
||||||
at: "left bottom",
|
|
||||||
// if called by 'open' method, event does not have pageX/Y
|
|
||||||
of: (event.pageX === undefined) ? event.target : event,
|
|
||||||
collision: "fit"
|
|
||||||
}, posOption);
|
|
||||||
|
|
||||||
// Finally display the popup
|
|
||||||
this.$menu
|
|
||||||
.show() // required to fix positioning error
|
|
||||||
.css({
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
top: 0
|
|
||||||
}).position(posOption)
|
|
||||||
.hide(); // hide again, so we can apply nice effects
|
|
||||||
|
|
||||||
if ( opts.preventContextMenuForPopup ) {
|
|
||||||
this.$menu.bind("contextmenu" + this.eventNamespace, function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this._show(this.$menu, opts.show, function() {
|
|
||||||
// Set focus to first active menu entry
|
|
||||||
if ( opts.autoFocus ) {
|
|
||||||
// var $first = self.$menu.children(".ui-menu-item:enabled:first");
|
|
||||||
// self.$menu.menu("focus", event, $first).focus();
|
|
||||||
self.$menu.focus();
|
|
||||||
self.previousFocus = $(event.target);
|
|
||||||
}
|
|
||||||
self._trigger.call(self, "open", event, ui);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/** Close popup. */
|
|
||||||
_closeMenu: function(immediately) {
|
|
||||||
var self = this,
|
|
||||||
hideOpts = immediately ? false : this.options.hide;
|
|
||||||
|
|
||||||
// Note: we don't want to unbind the 'contextmenu' event
|
|
||||||
$(document)
|
|
||||||
.unbind("mousedown" + this.eventNamespace)
|
|
||||||
.unbind("touchstart" + this.eventNamespace)
|
|
||||||
.unbind("keydown" + this.eventNamespace);
|
|
||||||
|
|
||||||
self.currentTarget = null; // issue #44 after hide animation is too late
|
|
||||||
if ( this.$menu ) { // #88: widget might have been destroyed already
|
|
||||||
this.$menu
|
|
||||||
.unbind("contextmenu" + this.eventNamespace);
|
|
||||||
this._hide(this.$menu, hideOpts, function() {
|
|
||||||
if ( self.previousFocus ) {
|
|
||||||
self.previousFocus.focus();
|
|
||||||
self.previousFocus = null;
|
|
||||||
}
|
|
||||||
self._trigger("close");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
self._trigger("close");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/** Handle $().contextmenu("option", key, value) calls. */
|
|
||||||
_setOption: function(key, value) {
|
|
||||||
switch (key) {
|
|
||||||
case "menu":
|
|
||||||
this.replaceMenu(value);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$.Widget.prototype._setOption.apply(this, arguments);
|
|
||||||
},
|
|
||||||
/** Return ui-menu entry (<LI> tag). */
|
|
||||||
_getMenuEntry: function(cmd) {
|
|
||||||
return this.$menu.find("li[data-command=" + cmd + "]");
|
|
||||||
},
|
|
||||||
/** Close context menu. */
|
|
||||||
close: function() {
|
|
||||||
if (this.isOpen()) {
|
|
||||||
this._closeMenu();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/** Enable or disable the menu command. */
|
|
||||||
enableEntry: function(cmd, flag) {
|
|
||||||
this._getMenuEntry(cmd).toggleClass("ui-state-disabled", (flag === false));
|
|
||||||
},
|
|
||||||
/** Return Menu element (UL). */
|
|
||||||
getMenu: function() {
|
|
||||||
return this.$menu;
|
|
||||||
},
|
|
||||||
/** Return true if menu is open. */
|
|
||||||
isOpen: function() {
|
|
||||||
// return this.$menu && this.$menu.is(":visible");
|
|
||||||
return !!this.$menu && !!this.currentTarget;
|
|
||||||
},
|
|
||||||
/** Open context menu on a specific target (must match options.delegate)
|
|
||||||
* Optional `extraData` is passed to event handlers as `ui.extraData`.
|
|
||||||
*/
|
|
||||||
open: function(target, extraData) {
|
|
||||||
// Fake a 'contextmenu' event
|
|
||||||
extraData = extraData || {};
|
|
||||||
var e = jQuery.Event("contextmenu", { target: target.get(0), extraData: extraData });
|
|
||||||
return this.element.trigger(e);
|
|
||||||
},
|
|
||||||
/** Replace the menu altogether. */
|
|
||||||
replaceMenu: function(data) {
|
|
||||||
this._createUiMenu(data);
|
|
||||||
},
|
|
||||||
/** Redefine menu entry (title or all of it). */
|
|
||||||
setEntry: function(cmd, entry) {
|
|
||||||
var $ul,
|
|
||||||
$entryLi = this._getMenuEntry(cmd);
|
|
||||||
|
|
||||||
if (typeof entry === "string") {
|
|
||||||
$.moogle.contextmenu.updateTitle($entryLi, entry);
|
|
||||||
} else {
|
|
||||||
$entryLi.empty();
|
|
||||||
entry.cmd = entry.cmd || cmd;
|
|
||||||
$.moogle.contextmenu.createEntryMarkup(entry, $entryLi);
|
|
||||||
if ($.isArray(entry.children)) {
|
|
||||||
$ul = $("<ul/>").appendTo($entryLi);
|
|
||||||
$.moogle.contextmenu.createMenuMarkup(entry.children, $ul);
|
|
||||||
}
|
|
||||||
this.getMenu().menu("refresh");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/** Show or hide the menu command. */
|
|
||||||
showEntry: function(cmd, flag) {
|
|
||||||
this._getMenuEntry(cmd).toggle(flag !== false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Global functions
|
|
||||||
*/
|
|
||||||
$.extend($.moogle.contextmenu, {
|
|
||||||
/** Convert a menu description into a into a <li> content. */
|
|
||||||
createEntryMarkup: function(entry, $parentLi) {
|
|
||||||
var $a = null;
|
|
||||||
|
|
||||||
if ( !/[^\-\u2014\u2013\s]/.test( entry.title ) ) {
|
|
||||||
// hyphen, em dash, en dash: separator as defined by UI Menu 1.10
|
|
||||||
$parentLi.text(entry.title);
|
|
||||||
} else {
|
|
||||||
if ( isLTE110 ) {
|
|
||||||
// jQuery UI Menu 1.10 or before required an `<a>` tag
|
|
||||||
$parentLi.attr("data-command", entry.cmd);
|
|
||||||
$a = $("<a/>", {
|
|
||||||
html: "" + entry.title,
|
|
||||||
href: "#"
|
|
||||||
}).appendTo($parentLi);
|
|
||||||
|
|
||||||
if ( entry.uiIcon ) {
|
|
||||||
$a.append($("<span class='ui-icon' />").addClass(entry.uiIcon));
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// jQuery UI Menu 1.11+ preferes to avoid `<a>` tags
|
|
||||||
$parentLi
|
|
||||||
.attr("data-command", entry.cmd)
|
|
||||||
.html("" + entry.title);
|
|
||||||
if ( $.isFunction(entry.action) ) {
|
|
||||||
$parentLi.data("actionHandler", entry.action);
|
|
||||||
}
|
|
||||||
if ( entry.uiIcon ) {
|
|
||||||
$parentLi
|
|
||||||
.append($("<span class='ui-icon' />")
|
|
||||||
.addClass(entry.uiIcon));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( $.isFunction(entry.action) ) {
|
|
||||||
$parentLi.data("actionHandler", entry.action);
|
|
||||||
}
|
|
||||||
if ( entry.disabled ) {
|
|
||||||
$parentLi.addClass("ui-state-disabled");
|
|
||||||
}
|
|
||||||
if ( entry.addClass ) {
|
|
||||||
$parentLi.addClass(entry.addClass);
|
|
||||||
}
|
|
||||||
if ( $.isPlainObject(entry.data) ) {
|
|
||||||
$parentLi.data(entry.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/** Convert a nested array of command objects into a <ul> structure. */
|
|
||||||
createMenuMarkup: function(options, $parentUl) {
|
|
||||||
var i, menu, $ul, $li;
|
|
||||||
if ( $parentUl == null ) {
|
|
||||||
$parentUl = $("<ul class='ui-helper-hidden' />").appendTo("body");
|
|
||||||
}
|
|
||||||
for (i = 0; i < options.length; i++) {
|
|
||||||
menu = options[i];
|
|
||||||
$li = $("<li/>").appendTo($parentUl);
|
|
||||||
|
|
||||||
$.moogle.contextmenu.createEntryMarkup(menu, $li);
|
|
||||||
|
|
||||||
if ( $.isArray(menu.children) ) {
|
|
||||||
$ul = $("<ul/>").appendTo($li);
|
|
||||||
$.moogle.contextmenu.createMenuMarkup(menu.children, $ul);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $parentUl;
|
|
||||||
},
|
|
||||||
/** Returns true if the menu item has child menu items */
|
|
||||||
isMenu: function(item) {
|
|
||||||
if ( isLTE110 ) {
|
|
||||||
return item.has(">a[aria-haspopup='true']").length > 0;
|
|
||||||
} else {
|
|
||||||
return item.is("[aria-haspopup='true']");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/** Replaces the value of elem's first text node child */
|
|
||||||
replaceFirstTextNodeChild: function(elem, text) {
|
|
||||||
elem
|
|
||||||
.contents()
|
|
||||||
.filter(function() { return this.nodeType === 3; })
|
|
||||||
.first()
|
|
||||||
.replaceWith(text);
|
|
||||||
},
|
|
||||||
/** Updates the menu item's title */
|
|
||||||
updateTitle: function(item, title) {
|
|
||||||
if ( isLTE110 ) {
|
|
||||||
$.moogle.contextmenu.replaceFirstTextNodeChild($("a", item), title);
|
|
||||||
} else {
|
|
||||||
$.moogle.contextmenu.replaceFirstTextNodeChild(item, title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}));
|
|
|
@ -1,53 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright © Dave Perrett, Malcolm Jarvis and Artem Vorotnikov
|
|
||||||
*
|
|
||||||
* This file is licensed under the GPLv2.
|
|
||||||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
// IE specific fixes here
|
|
||||||
if (jQuery.browser.msie) {
|
|
||||||
try {
|
|
||||||
document.execCommand('BackgroundImageCache', false, true);
|
|
||||||
} catch (err) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jQuery.browser.safari) {
|
|
||||||
// Move search field's margin down for the styled input
|
|
||||||
document.getElementById('torrent_search').style['margin-top'] = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMobileDevice) {
|
|
||||||
window.onload = function () {
|
|
||||||
setTimeout(function () {
|
|
||||||
window.scrollTo(0, 1);
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
window.onorientationchange = function () {
|
|
||||||
setTimeout(function () {
|
|
||||||
window.scrollTo(0, 1);
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
if (window.navigator.standalone) {
|
|
||||||
// Fix min height for isMobileDevice when run in full screen mode from home screen
|
|
||||||
// so the footer appears in the right place
|
|
||||||
document.getElementById('torrent_container').style['min-height'] = '338px';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fix for non-Safari-3 browsers: dark borders to replace shadows.
|
|
||||||
Array.from(document.getElementsByClassName('dialog_window')).forEach(function (e) {
|
|
||||||
e.style['border'] = '1px solid #777';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialise the dialog controller
|
|
||||||
dialog = new Dialog();
|
|
||||||
|
|
||||||
// Initialise the main Transmission controller
|
|
||||||
transmission = new Transmission();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', main);
|
|
|
@ -1,46 +0,0 @@
|
||||||
const Notifications = {};
|
|
||||||
|
|
||||||
$(document).ready(function () {
|
|
||||||
if (!window.webkitNotifications) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let notificationsEnabled = window.webkitNotifications.checkPermission() === 0;
|
|
||||||
const toggle = $('#toggle_notifications');
|
|
||||||
|
|
||||||
toggle.show();
|
|
||||||
updateMenuTitle();
|
|
||||||
$(transmission).bind('downloadComplete seedingComplete', function (event, torrent) {
|
|
||||||
if (notificationsEnabled) {
|
|
||||||
let title = (event.type == 'downloadComplete' ? 'Download' : 'Seeding') + ' complete',
|
|
||||||
content = torrent.getName(),
|
|
||||||
notification;
|
|
||||||
|
|
||||||
notification = window.webkitNotifications.createNotification(
|
|
||||||
'style/transmission/images/logo.png',
|
|
||||||
title,
|
|
||||||
content
|
|
||||||
);
|
|
||||||
notification.show();
|
|
||||||
setTimeout(function () {
|
|
||||||
notification.cancel();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateMenuTitle() {
|
|
||||||
toggle.html((notificationsEnabled ? 'Disable' : 'Enable') + ' Notifications');
|
|
||||||
}
|
|
||||||
|
|
||||||
Notifications.toggle = function () {
|
|
||||||
if (window.webkitNotifications.checkPermission() !== 0) {
|
|
||||||
window.webkitNotifications.requestPermission(function () {
|
|
||||||
notificationsEnabled = window.webkitNotifications.checkPermission() === 0;
|
|
||||||
updateMenuTitle();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
notificationsEnabled = !notificationsEnabled;
|
|
||||||
updateMenuTitle();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
|
@ -1,87 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright © Dave Perrett, Malcolm Jarvis and Artem Vorotnikov
|
|
||||||
*
|
|
||||||
* This file is licensed under the GPLv2.
|
|
||||||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!Array.from) {
|
|
||||||
Array.from = (function () {
|
|
||||||
const toStr = Object.prototype.toString;
|
|
||||||
const isCallable = function (fn) {
|
|
||||||
return typeof fn === 'function' || toStr.call(fn) === '[object Function]';
|
|
||||||
};
|
|
||||||
const toInteger = function (value) {
|
|
||||||
const number = Number(value);
|
|
||||||
if (isNaN(number)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (number === 0 || !isFinite(number)) {
|
|
||||||
return number;
|
|
||||||
}
|
|
||||||
return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
|
|
||||||
};
|
|
||||||
const maxSafeInteger = Math.pow(2, 53) - 1;
|
|
||||||
const toLength = function (value) {
|
|
||||||
const len = toInteger(value);
|
|
||||||
return Math.min(Math.max(len, 0), maxSafeInteger);
|
|
||||||
};
|
|
||||||
|
|
||||||
// The length property of the from method is 1.
|
|
||||||
return function from(arrayLike /*, mapFn, thisArg */) {
|
|
||||||
// 1. Let C be the this value.
|
|
||||||
const C = this;
|
|
||||||
|
|
||||||
// 2. Let items be ToObject(arrayLike).
|
|
||||||
const items = Object(arrayLike);
|
|
||||||
|
|
||||||
// 3. ReturnIfAbrupt(items).
|
|
||||||
if (arrayLike == null) {
|
|
||||||
throw new TypeError('Array.from requires an array-like object - not null or undefined');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. If mapfn is undefined, then let mapping be false.
|
|
||||||
const mapFn = arguments.length > 1 ? arguments[1] : void undefined;
|
|
||||||
let T;
|
|
||||||
if (typeof mapFn !== 'undefined') {
|
|
||||||
// 5. else
|
|
||||||
// 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
|
|
||||||
if (!isCallable(mapFn)) {
|
|
||||||
throw new TypeError('Array.from: when provided, the second argument must be a function');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
|
||||||
if (arguments.length > 2) {
|
|
||||||
T = arguments[2];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 10. Let lenValue be Get(items, "length").
|
|
||||||
// 11. Let len be ToLength(lenValue).
|
|
||||||
const len = toLength(items.length);
|
|
||||||
|
|
||||||
// 13. If IsConstructor(C) is true, then
|
|
||||||
// 13. a. Let A be the result of calling the [[Construct]] internal method of C with an argument list containing the single item len.
|
|
||||||
// 14. a. Else, Let A be ArrayCreate(len).
|
|
||||||
const A = isCallable(C) ? Object(new C(len)) : new Array(len);
|
|
||||||
|
|
||||||
// 16. Let k be 0.
|
|
||||||
let k = 0;
|
|
||||||
// 17. Repeat, while k < len… (also steps a - h)
|
|
||||||
let kValue;
|
|
||||||
while (k < len) {
|
|
||||||
kValue = items[k];
|
|
||||||
if (mapFn) {
|
|
||||||
A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k);
|
|
||||||
} else {
|
|
||||||
A[k] = kValue;
|
|
||||||
}
|
|
||||||
k += 1;
|
|
||||||
}
|
|
||||||
// 18. Let putStatus be Put(A, "length", len, true).
|
|
||||||
A.length = len;
|
|
||||||
// 20. Return A.
|
|
||||||
return A;
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
}
|
|
|
@ -1,308 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright © Charles Kerr, Dave Perrett, Malcolm Jarvis and Bruno Bierbaumer
|
|
||||||
*
|
|
||||||
* This file is licensed under the GPLv2.
|
|
||||||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
function PrefsDialog(remote) {
|
|
||||||
const data = {
|
|
||||||
dialog: null,
|
|
||||||
remote: null,
|
|
||||||
elements: {},
|
|
||||||
|
|
||||||
// all the RPC session keys that we have gui controls for
|
|
||||||
keys: [
|
|
||||||
'alt-speed-down',
|
|
||||||
'alt-speed-time-begin',
|
|
||||||
'alt-speed-time-day',
|
|
||||||
'alt-speed-time-enabled',
|
|
||||||
'alt-speed-time-end',
|
|
||||||
'alt-speed-up',
|
|
||||||
'blocklist-enabled',
|
|
||||||
'blocklist-size',
|
|
||||||
'blocklist-url',
|
|
||||||
'dht-enabled',
|
|
||||||
'download-dir',
|
|
||||||
'encryption',
|
|
||||||
'idle-seeding-limit',
|
|
||||||
'idle-seeding-limit-enabled',
|
|
||||||
'lpd-enabled',
|
|
||||||
'peer-limit-global',
|
|
||||||
'peer-limit-per-torrent',
|
|
||||||
'peer-port',
|
|
||||||
'peer-port-random-on-start',
|
|
||||||
'pex-enabled',
|
|
||||||
'port-forwarding-enabled',
|
|
||||||
'rename-partial-files',
|
|
||||||
'seedRatioLimit',
|
|
||||||
'seedRatioLimited',
|
|
||||||
'speed-limit-down',
|
|
||||||
'speed-limit-down-enabled',
|
|
||||||
'speed-limit-up',
|
|
||||||
'speed-limit-up-enabled',
|
|
||||||
'start-added-torrents',
|
|
||||||
'utp-enabled',
|
|
||||||
],
|
|
||||||
|
|
||||||
// map of keys that are enabled only if a 'parent' key is enabled
|
|
||||||
groups: {
|
|
||||||
'alt-speed-time-enabled': [
|
|
||||||
'alt-speed-time-begin',
|
|
||||||
'alt-speed-time-day',
|
|
||||||
'alt-speed-time-end',
|
|
||||||
],
|
|
||||||
'blocklist-enabled': ['blocklist-url', 'blocklist-update-button'],
|
|
||||||
'idle-seeding-limit-enabled': ['idle-seeding-limit'],
|
|
||||||
seedRatioLimited: ['seedRatioLimit'],
|
|
||||||
'speed-limit-down-enabled': ['speed-limit-down'],
|
|
||||||
'speed-limit-up-enabled': ['speed-limit-up'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const initTimeDropDown = function (e) {
|
|
||||||
let i, hour, mins, value, content;
|
|
||||||
|
|
||||||
for (i = 0; i < 24 * 4; ++i) {
|
|
||||||
hour = parseInt(i / 4, 10);
|
|
||||||
mins = (i % 4) * 15;
|
|
||||||
value = i * 15;
|
|
||||||
content = hour + ':' + (mins || '00');
|
|
||||||
e.options[i] = new Option(content, value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPortChecked = function (response) {
|
|
||||||
const is_open = response['arguments']['port-is-open'];
|
|
||||||
const text = 'Port is <b>' + (is_open ? 'Open' : 'Closed') + '</b>';
|
|
||||||
const e = data.elements.root.find('#port-label');
|
|
||||||
setInnerHTML(e[0], text);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setGroupEnabled = function (parent_key, enabled) {
|
|
||||||
let i, key, keys, root;
|
|
||||||
|
|
||||||
if (parent_key in data.groups) {
|
|
||||||
root = data.elements.root;
|
|
||||||
keys = data.groups[parent_key];
|
|
||||||
|
|
||||||
for (i = 0; (key = keys[i]); ++i) {
|
|
||||||
root.find('#' + key).attr('disabled', !enabled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onBlocklistUpdateClicked = function () {
|
|
||||||
data.remote.updateBlocklist();
|
|
||||||
setBlocklistButtonEnabled(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
var setBlocklistButtonEnabled = function (b) {
|
|
||||||
const e = data.elements.blocklist_button;
|
|
||||||
e.attr('disabled', !b);
|
|
||||||
e.val(b ? 'Update' : 'Updating...');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getValue = function (e) {
|
|
||||||
let str;
|
|
||||||
|
|
||||||
switch (e[0].type) {
|
|
||||||
case 'checkbox':
|
|
||||||
case 'radio':
|
|
||||||
return e.prop('checked');
|
|
||||||
|
|
||||||
case 'text':
|
|
||||||
case 'url':
|
|
||||||
case 'email':
|
|
||||||
case 'number':
|
|
||||||
case 'search':
|
|
||||||
case 'select-one':
|
|
||||||
str = e.val();
|
|
||||||
if (parseInt(str, 10).toString() === str) {
|
|
||||||
return parseInt(str, 10);
|
|
||||||
}
|
|
||||||
if (parseFloat(str).toString() === str) {
|
|
||||||
return parseFloat(str);
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* this callback is for controls whose changes can be applied
|
|
||||||
immediately, like checkboxs, radioboxes, and selects */
|
|
||||||
const onControlChanged = function (ev) {
|
|
||||||
const o = {};
|
|
||||||
o[ev.target.id] = getValue($(ev.target));
|
|
||||||
data.remote.savePrefs(o);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* these two callbacks are for controls whose changes can't be applied
|
|
||||||
immediately -- like a text entry field -- because it takes many
|
|
||||||
change events for the user to get to the desired result */
|
|
||||||
const onControlFocused = function (ev) {
|
|
||||||
data.oldValue = getValue($(ev.target));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onControlBlurred = function (ev) {
|
|
||||||
const newValue = getValue($(ev.target));
|
|
||||||
if (newValue !== data.oldValue) {
|
|
||||||
const o = {};
|
|
||||||
o[ev.target.id] = newValue;
|
|
||||||
data.remote.savePrefs(o);
|
|
||||||
delete data.oldValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDefaultMobileOptions = function () {
|
|
||||||
return {
|
|
||||||
width: $(window).width(),
|
|
||||||
height: $(window).height(),
|
|
||||||
position: ['left', 'top'],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialize = function (remote) {
|
|
||||||
let i, key, e, o;
|
|
||||||
|
|
||||||
data.remote = remote;
|
|
||||||
|
|
||||||
e = $('#prefs-dialog');
|
|
||||||
data.elements.root = e;
|
|
||||||
|
|
||||||
initTimeDropDown(e.find('#alt-speed-time-begin')[0]);
|
|
||||||
initTimeDropDown(e.find('#alt-speed-time-end')[0]);
|
|
||||||
|
|
||||||
o = isMobileDevice
|
|
||||||
? getDefaultMobileOptions()
|
|
||||||
: {
|
|
||||||
width: 350,
|
|
||||||
height: 400,
|
|
||||||
};
|
|
||||||
o.autoOpen = false;
|
|
||||||
o.show = o.hide = 'fade';
|
|
||||||
o.close = onDialogClosed;
|
|
||||||
e.tabbedDialog(o);
|
|
||||||
|
|
||||||
e = e.find('#blocklist-update-button');
|
|
||||||
data.elements.blocklist_button = e;
|
|
||||||
e.click(onBlocklistUpdateClicked);
|
|
||||||
|
|
||||||
// listen for user input
|
|
||||||
for (i = 0; (key = data.keys[i]); ++i) {
|
|
||||||
e = data.elements.root.find('#' + key);
|
|
||||||
switch (e[0].type) {
|
|
||||||
case 'checkbox':
|
|
||||||
case 'radio':
|
|
||||||
case 'select-one':
|
|
||||||
e.change(onControlChanged);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'text':
|
|
||||||
case 'url':
|
|
||||||
case 'email':
|
|
||||||
case 'number':
|
|
||||||
case 'search':
|
|
||||||
e.focus(onControlFocused);
|
|
||||||
e.blur(onControlBlurred);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getValues = function () {
|
|
||||||
let i,
|
|
||||||
key,
|
|
||||||
val,
|
|
||||||
o = {},
|
|
||||||
keys = data.keys,
|
|
||||||
root = data.elements.root;
|
|
||||||
|
|
||||||
for (i = 0; (key = keys[i]); ++i) {
|
|
||||||
val = getValue(root.find('#' + key));
|
|
||||||
if (val !== null) {
|
|
||||||
o[key] = val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return o;
|
|
||||||
};
|
|
||||||
|
|
||||||
var onDialogClosed = function () {
|
|
||||||
transmission.hideMobileAddressbar();
|
|
||||||
|
|
||||||
$(data.dialog).trigger('closed', getValues());
|
|
||||||
};
|
|
||||||
|
|
||||||
/****
|
|
||||||
***** PUBLIC FUNCTIONS
|
|
||||||
****/
|
|
||||||
|
|
||||||
// update the dialog's controls
|
|
||||||
this.set = function (o) {
|
|
||||||
let e, i, key, val;
|
|
||||||
const keys = data.keys;
|
|
||||||
const root = data.elements.root;
|
|
||||||
|
|
||||||
setBlocklistButtonEnabled(true);
|
|
||||||
|
|
||||||
for (i = 0; (key = keys[i]); ++i) {
|
|
||||||
val = o[key];
|
|
||||||
e = root.find('#' + key);
|
|
||||||
|
|
||||||
if (key === 'blocklist-size') {
|
|
||||||
// special case -- regular text area
|
|
||||||
e.text('' + val.toStringWithCommas());
|
|
||||||
} else {
|
|
||||||
switch (e[0].type) {
|
|
||||||
case 'checkbox':
|
|
||||||
case 'radio':
|
|
||||||
e.prop('checked', val);
|
|
||||||
setGroupEnabled(key, val);
|
|
||||||
break;
|
|
||||||
case 'text':
|
|
||||||
case 'url':
|
|
||||||
case 'email':
|
|
||||||
case 'number':
|
|
||||||
case 'search':
|
|
||||||
// don't change the text if the user's editing it.
|
|
||||||
// it's very annoying when that happens!
|
|
||||||
if (e[0] !== document.activeElement) {
|
|
||||||
e.val(val);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'select-one':
|
|
||||||
e.val(val);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.show = function () {
|
|
||||||
transmission.hideMobileAddressbar();
|
|
||||||
|
|
||||||
setBlocklistButtonEnabled(true);
|
|
||||||
data.remote.checkPort(onPortChecked, this);
|
|
||||||
data.elements.root.dialog('open');
|
|
||||||
};
|
|
||||||
|
|
||||||
this.close = function () {
|
|
||||||
transmission.hideMobileAddressbar();
|
|
||||||
data.elements.root.dialog('close');
|
|
||||||
};
|
|
||||||
|
|
||||||
this.shouldAddedTorrentsStart = function () {
|
|
||||||
return data.elements.root.find('#start-added-torrents')[0].checked;
|
|
||||||
};
|
|
||||||
|
|
||||||
data.dialog = this;
|
|
||||||
initialize(remote);
|
|
||||||
}
|
|
|
@ -1,302 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright © Charles Kerr, Dave Perrett, Malcolm Jarvis and Bruno Bierbaumer
|
|
||||||
*
|
|
||||||
* This file is licensed under the GPLv2.
|
|
||||||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
const RPC = {
|
|
||||||
_DaemonVersion: 'version',
|
|
||||||
_DownSpeedLimit: 'speed-limit-down',
|
|
||||||
_DownSpeedLimited: 'speed-limit-down-enabled',
|
|
||||||
_QueueMoveTop: 'queue-move-top',
|
|
||||||
_QueueMoveBottom: 'queue-move-bottom',
|
|
||||||
_QueueMoveUp: 'queue-move-up',
|
|
||||||
_QueueMoveDown: 'queue-move-down',
|
|
||||||
_Root: '../rpc',
|
|
||||||
_TurtleDownSpeedLimit: 'alt-speed-down',
|
|
||||||
_TurtleState: 'alt-speed-enabled',
|
|
||||||
_TurtleUpSpeedLimit: 'alt-speed-up',
|
|
||||||
_UpSpeedLimit: 'speed-limit-up',
|
|
||||||
_UpSpeedLimited: 'speed-limit-up-enabled',
|
|
||||||
};
|
|
||||||
|
|
||||||
function TransmissionRemote(controller) {
|
|
||||||
this.initialize(controller);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
TransmissionRemote.prototype = {
|
|
||||||
/*
|
|
||||||
* Constructor
|
|
||||||
*/
|
|
||||||
initialize: function (controller) {
|
|
||||||
this._controller = controller;
|
|
||||||
this._error = '';
|
|
||||||
this._token = '';
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Display an error if an ajax request fails, and stop sending requests
|
|
||||||
* or on a 409, globally set the X-Transmission-Session-Id and resend
|
|
||||||
*/
|
|
||||||
ajaxError: function (request, error_string, exception, ajaxObject) {
|
|
||||||
let token;
|
|
||||||
const remote = this;
|
|
||||||
|
|
||||||
// set the Transmission-Session-Id on a 409
|
|
||||||
if (
|
|
||||||
request.status === 409 &&
|
|
||||||
(token = request.getResponseHeader('X-Transmission-Session-Id'))
|
|
||||||
) {
|
|
||||||
remote._token = token;
|
|
||||||
$.ajax(ajaxObject);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
remote._error = request.responseText
|
|
||||||
? request.responseText.trim().replace(/(<([^>]+)>)/gi, '')
|
|
||||||
: '';
|
|
||||||
if (!remote._error.length) {
|
|
||||||
remote._error = 'Server not responding';
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.confirm(
|
|
||||||
'Connection Failed',
|
|
||||||
'Could not connect to the server. You may need to reload the page to reconnect.',
|
|
||||||
'Details',
|
|
||||||
function () {
|
|
||||||
alert(remote._error);
|
|
||||||
},
|
|
||||||
'Dismiss'
|
|
||||||
);
|
|
||||||
remote._controller.togglePeriodicSessionRefresh(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
appendSessionId: function (XHR) {
|
|
||||||
if (this._token) {
|
|
||||||
XHR.setRequestHeader('X-Transmission-Session-Id', this._token);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
sendRequest: function (data, callback, context, async) {
|
|
||||||
const remote = this;
|
|
||||||
if (typeof async != 'boolean') {
|
|
||||||
async = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ajaxSettings = {
|
|
||||||
url: RPC._Root,
|
|
||||||
type: 'POST',
|
|
||||||
contentType: 'json',
|
|
||||||
dataType: 'json',
|
|
||||||
cache: false,
|
|
||||||
data: JSON.stringify(data),
|
|
||||||
beforeSend: function (XHR) {
|
|
||||||
remote.appendSessionId(XHR);
|
|
||||||
},
|
|
||||||
error: function (request, error_string, exception) {
|
|
||||||
remote.ajaxError(request, error_string, exception, ajaxSettings);
|
|
||||||
},
|
|
||||||
success: callback,
|
|
||||||
context: context,
|
|
||||||
async: async,
|
|
||||||
};
|
|
||||||
|
|
||||||
$.ajax(ajaxSettings);
|
|
||||||
},
|
|
||||||
|
|
||||||
loadDaemonPrefs: function (callback, context, async) {
|
|
||||||
const o = {
|
|
||||||
method: 'session-get',
|
|
||||||
};
|
|
||||||
this.sendRequest(o, callback, context, async);
|
|
||||||
},
|
|
||||||
|
|
||||||
checkPort: function (callback, context, async) {
|
|
||||||
const o = {
|
|
||||||
method: 'port-test',
|
|
||||||
};
|
|
||||||
this.sendRequest(o, callback, context, async);
|
|
||||||
},
|
|
||||||
|
|
||||||
renameTorrent: function (torrentIds, oldpath, newname, callback, context) {
|
|
||||||
const o = {
|
|
||||||
method: 'torrent-rename-path',
|
|
||||||
arguments: {
|
|
||||||
ids: torrentIds,
|
|
||||||
path: oldpath,
|
|
||||||
name: newname,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.sendRequest(o, callback, context);
|
|
||||||
},
|
|
||||||
|
|
||||||
loadDaemonStats: function (callback, context, async) {
|
|
||||||
const o = {
|
|
||||||
method: 'session-stats',
|
|
||||||
};
|
|
||||||
this.sendRequest(o, callback, context, async);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateTorrents: function (torrentIds, fields, callback, context) {
|
|
||||||
const o = {
|
|
||||||
method: 'torrent-get',
|
|
||||||
arguments: {
|
|
||||||
fields: fields,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (torrentIds) {
|
|
||||||
o['arguments'].ids = torrentIds;
|
|
||||||
}
|
|
||||||
this.sendRequest(o, function (response) {
|
|
||||||
const args = response['arguments'];
|
|
||||||
callback.call(context, args.torrents, args.removed);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getFreeSpace: function (dir, callback, context) {
|
|
||||||
const o = {
|
|
||||||
method: 'free-space',
|
|
||||||
arguments: {
|
|
||||||
path: dir,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.sendRequest(o, function (response) {
|
|
||||||
const args = response['arguments'];
|
|
||||||
callback.call(context, args.path, args['size-bytes']);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
changeFileCommand: function (torrentId, fileIndices, command) {
|
|
||||||
const remote = this,
|
|
||||||
args = {
|
|
||||||
ids: [torrentId],
|
|
||||||
};
|
|
||||||
args[command] = fileIndices;
|
|
||||||
this.sendRequest(
|
|
||||||
{
|
|
||||||
arguments: args,
|
|
||||||
method: 'torrent-set',
|
|
||||||
},
|
|
||||||
function () {
|
|
||||||
remote._controller.refreshTorrents([torrentId]);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
sendTorrentSetRequests: function (method, torrent_ids, args, callback, context) {
|
|
||||||
if (!args) {
|
|
||||||
args = {};
|
|
||||||
}
|
|
||||||
args['ids'] = torrent_ids;
|
|
||||||
const o = {
|
|
||||||
method: method,
|
|
||||||
arguments: args,
|
|
||||||
};
|
|
||||||
this.sendRequest(o, callback, context);
|
|
||||||
},
|
|
||||||
|
|
||||||
sendTorrentActionRequests: function (method, torrent_ids, callback, context) {
|
|
||||||
this.sendTorrentSetRequests(method, torrent_ids, null, callback, context);
|
|
||||||
},
|
|
||||||
|
|
||||||
startTorrents: function (torrent_ids, noqueue, callback, context) {
|
|
||||||
const name = noqueue ? 'torrent-start-now' : 'torrent-start';
|
|
||||||
this.sendTorrentActionRequests(name, torrent_ids, callback, context);
|
|
||||||
},
|
|
||||||
stopTorrents: function (torrent_ids, callback, context) {
|
|
||||||
this.sendTorrentActionRequests('torrent-stop', torrent_ids, callback, context);
|
|
||||||
},
|
|
||||||
|
|
||||||
moveTorrents: function (torrent_ids, new_location, callback, context) {
|
|
||||||
this.sendTorrentSetRequests(
|
|
||||||
'torrent-set-location',
|
|
||||||
torrent_ids,
|
|
||||||
{
|
|
||||||
move: true,
|
|
||||||
location: new_location,
|
|
||||||
},
|
|
||||||
callback,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
removeTorrents: function (torrent_ids, callback, context) {
|
|
||||||
this.sendTorrentActionRequests('torrent-remove', torrent_ids, callback, context);
|
|
||||||
},
|
|
||||||
removeTorrentsAndData: function (torrents) {
|
|
||||||
const remote = this;
|
|
||||||
const o = {
|
|
||||||
method: 'torrent-remove',
|
|
||||||
arguments: {
|
|
||||||
'delete-local-data': true,
|
|
||||||
ids: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (torrents) {
|
|
||||||
for (let i = 0, len = torrents.length; i < len; ++i) {
|
|
||||||
o.arguments.ids.push(torrents[i].getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.sendRequest(o, function () {
|
|
||||||
remote._controller.refreshTorrents();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
verifyTorrents: function (torrent_ids, callback, context) {
|
|
||||||
this.sendTorrentActionRequests('torrent-verify', torrent_ids, callback, context);
|
|
||||||
},
|
|
||||||
reannounceTorrents: function (torrent_ids, callback, context) {
|
|
||||||
this.sendTorrentActionRequests('torrent-reannounce', torrent_ids, callback, context);
|
|
||||||
},
|
|
||||||
addTorrentByUrl: function (url, options) {
|
|
||||||
const remote = this;
|
|
||||||
if (url.match(/^[0-9a-f]{40}$/i)) {
|
|
||||||
url = 'magnet:?xt=urn:btih:' + url;
|
|
||||||
}
|
|
||||||
const o = {
|
|
||||||
method: 'torrent-add',
|
|
||||||
arguments: {
|
|
||||||
paused: options.paused,
|
|
||||||
filename: url,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.sendRequest(o, function () {
|
|
||||||
remote._controller.refreshTorrents();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
savePrefs: function (args) {
|
|
||||||
const remote = this;
|
|
||||||
const o = {
|
|
||||||
method: 'session-set',
|
|
||||||
arguments: args,
|
|
||||||
};
|
|
||||||
this.sendRequest(o, function () {
|
|
||||||
remote._controller.loadDaemonPrefs();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
updateBlocklist: function () {
|
|
||||||
const remote = this;
|
|
||||||
const o = {
|
|
||||||
method: 'blocklist-update',
|
|
||||||
};
|
|
||||||
this.sendRequest(o, function () {
|
|
||||||
remote._controller.loadDaemonPrefs();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Added queue calls
|
|
||||||
moveTorrentsToTop: function (torrent_ids, callback, context) {
|
|
||||||
this.sendTorrentActionRequests(RPC._QueueMoveTop, torrent_ids, callback, context);
|
|
||||||
},
|
|
||||||
moveTorrentsToBottom: function (torrent_ids, callback, context) {
|
|
||||||
this.sendTorrentActionRequests(RPC._QueueMoveBottom, torrent_ids, callback, context);
|
|
||||||
},
|
|
||||||
moveTorrentsUp: function (torrent_ids, callback, context) {
|
|
||||||
this.sendTorrentActionRequests(RPC._QueueMoveUp, torrent_ids, callback, context);
|
|
||||||
},
|
|
||||||
moveTorrentsDown: function (torrent_ids, callback, context) {
|
|
||||||
this.sendTorrentActionRequests(RPC._QueueMoveDown, torrent_ids, callback, context);
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,451 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright © Mnemosyne LLC
|
|
||||||
*
|
|
||||||
* This file is licensed under the GPLv2.
|
|
||||||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
function TorrentRendererHelper() {}
|
|
||||||
|
|
||||||
TorrentRendererHelper.getProgressInfo = function (controller, t) {
|
|
||||||
let pct, extra;
|
|
||||||
const s = t.getStatus();
|
|
||||||
const seed_ratio_limit = t.seedRatioLimit(controller);
|
|
||||||
|
|
||||||
if (t.needsMetaData()) {
|
|
||||||
pct = t.getMetadataPercentComplete() * 100;
|
|
||||||
} else if (!t.isDone()) {
|
|
||||||
pct = Math.round(t.getPercentDone() * 100);
|
|
||||||
} else if (seed_ratio_limit > 0 && t.isSeeding()) {
|
|
||||||
// don't split up the bar if paused or queued
|
|
||||||
pct = Math.round((t.getUploadRatio() * 100) / seed_ratio_limit);
|
|
||||||
} else {
|
|
||||||
pct = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s === Torrent._StatusStopped) {
|
|
||||||
extra = 'paused';
|
|
||||||
} else if (s === Torrent._StatusDownloadWait) {
|
|
||||||
extra = 'leeching queued';
|
|
||||||
} else if (t.needsMetaData()) {
|
|
||||||
extra = 'magnet';
|
|
||||||
} else if (s === Torrent._StatusDownload) {
|
|
||||||
extra = 'leeching';
|
|
||||||
} else if (s === Torrent._StatusSeedWait) {
|
|
||||||
extra = 'seeding queued';
|
|
||||||
} else if (s === Torrent._StatusSeed) {
|
|
||||||
extra = 'seeding';
|
|
||||||
} else {
|
|
||||||
extra = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
percent: pct,
|
|
||||||
complete: ['torrent_progress_bar', 'complete', extra].join(' '),
|
|
||||||
incomplete: ['torrent_progress_bar', 'incomplete', extra].join(' '),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
TorrentRendererHelper.createProgressbar = function (classes) {
|
|
||||||
let complete, incomplete, progressbar;
|
|
||||||
|
|
||||||
complete = document.createElement('div');
|
|
||||||
complete.className = 'torrent_progress_bar complete';
|
|
||||||
|
|
||||||
incomplete = document.createElement('div');
|
|
||||||
incomplete.className = 'torrent_progress_bar incomplete';
|
|
||||||
|
|
||||||
progressbar = document.createElement('div');
|
|
||||||
progressbar.className = 'torrent_progress_bar_container ' + classes;
|
|
||||||
progressbar.appendChild(complete);
|
|
||||||
progressbar.appendChild(incomplete);
|
|
||||||
|
|
||||||
return {
|
|
||||||
element: progressbar,
|
|
||||||
complete: complete,
|
|
||||||
incomplete: incomplete,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
TorrentRendererHelper.renderProgressbar = function (controller, t, progressbar) {
|
|
||||||
let e, style, width, display;
|
|
||||||
const info = TorrentRendererHelper.getProgressInfo(controller, t);
|
|
||||||
|
|
||||||
// update the complete progressbar
|
|
||||||
e = progressbar.complete;
|
|
||||||
style = e.style;
|
|
||||||
width = '' + info.percent + '%';
|
|
||||||
display = info.percent > 0 ? 'block' : 'none';
|
|
||||||
if (style.width !== width || style.display !== display) {
|
|
||||||
$(e).css({
|
|
||||||
width: '' + info.percent + '%',
|
|
||||||
display: display,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.className !== info.complete) {
|
|
||||||
e.className = info.complete;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the incomplete progressbar
|
|
||||||
e = progressbar.incomplete;
|
|
||||||
display = info.percent < 100 ? 'block' : 'none';
|
|
||||||
|
|
||||||
if (e.style.display !== display) {
|
|
||||||
e.style.display = display;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.className !== info.incomplete) {
|
|
||||||
e.className = info.incomplete;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TorrentRendererHelper.formatUL = function (t) {
|
|
||||||
return '▲' + Transmission.fmt.speedBps(t.getUploadSpeed());
|
|
||||||
};
|
|
||||||
|
|
||||||
TorrentRendererHelper.formatDL = function (t) {
|
|
||||||
return '▼' + Transmission.fmt.speedBps(t.getDownloadSpeed());
|
|
||||||
};
|
|
||||||
|
|
||||||
TorrentRendererHelper.formatETA = function (t) {
|
|
||||||
const eta = t.getETA();
|
|
||||||
if (eta < 0 || eta >= 999 * 60 * 60) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return 'ETA: ' + Transmission.fmt.timeInterval(eta);
|
|
||||||
};
|
|
||||||
|
|
||||||
/****
|
|
||||||
*****
|
|
||||||
*****
|
|
||||||
****/
|
|
||||||
|
|
||||||
function TorrentRendererFull() {}
|
|
||||||
TorrentRendererFull.prototype = {
|
|
||||||
createRow: function () {
|
|
||||||
let root, name, peers, progressbar, details, image, button;
|
|
||||||
|
|
||||||
root = document.createElement('li');
|
|
||||||
root.className = 'torrent';
|
|
||||||
|
|
||||||
name = document.createElement('div');
|
|
||||||
name.className = 'torrent_name';
|
|
||||||
|
|
||||||
peers = document.createElement('div');
|
|
||||||
peers.className = 'torrent_peer_details';
|
|
||||||
|
|
||||||
progressbar = TorrentRendererHelper.createProgressbar('full');
|
|
||||||
|
|
||||||
details = document.createElement('div');
|
|
||||||
details.className = 'torrent_progress_details';
|
|
||||||
|
|
||||||
image = document.createElement('div');
|
|
||||||
button = document.createElement('a');
|
|
||||||
button.appendChild(image);
|
|
||||||
|
|
||||||
root.appendChild(name);
|
|
||||||
root.appendChild(peers);
|
|
||||||
root.appendChild(button);
|
|
||||||
root.appendChild(progressbar.element);
|
|
||||||
root.appendChild(details);
|
|
||||||
|
|
||||||
root._name_container = name;
|
|
||||||
root._peer_details_container = peers;
|
|
||||||
root._progress_details_container = details;
|
|
||||||
root._progressbar = progressbar;
|
|
||||||
root._pause_resume_button_image = image;
|
|
||||||
root._toggle_running_button = button;
|
|
||||||
|
|
||||||
return root;
|
|
||||||
},
|
|
||||||
|
|
||||||
getPeerDetails: function (t) {
|
|
||||||
let err,
|
|
||||||
peer_count,
|
|
||||||
webseed_count,
|
|
||||||
fmt = Transmission.fmt;
|
|
||||||
|
|
||||||
if ((err = t.getErrorMessage())) {
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t.isDownloading()) {
|
|
||||||
peer_count = t.getPeersConnected();
|
|
||||||
webseed_count = t.getWebseedsSendingToUs();
|
|
||||||
|
|
||||||
if (webseed_count && peer_count) {
|
|
||||||
// Downloading from 2 of 3 peer(s) and 2 webseed(s)
|
|
||||||
return [
|
|
||||||
'Downloading from',
|
|
||||||
t.getPeersSendingToUs(),
|
|
||||||
'of',
|
|
||||||
fmt.countString('peer', 'peers', peer_count),
|
|
||||||
'and',
|
|
||||||
fmt.countString('web seed', 'web seeds', webseed_count),
|
|
||||||
'–',
|
|
||||||
TorrentRendererHelper.formatDL(t),
|
|
||||||
TorrentRendererHelper.formatUL(t),
|
|
||||||
].join(' ');
|
|
||||||
} else if (webseed_count) {
|
|
||||||
// Downloading from 2 webseed(s)
|
|
||||||
return [
|
|
||||||
'Downloading from',
|
|
||||||
fmt.countString('web seed', 'web seeds', webseed_count),
|
|
||||||
'–',
|
|
||||||
TorrentRendererHelper.formatDL(t),
|
|
||||||
TorrentRendererHelper.formatUL(t),
|
|
||||||
].join(' ');
|
|
||||||
} else {
|
|
||||||
// Downloading from 2 of 3 peer(s)
|
|
||||||
return [
|
|
||||||
'Downloading from',
|
|
||||||
t.getPeersSendingToUs(),
|
|
||||||
'of',
|
|
||||||
fmt.countString('peer', 'peers', peer_count),
|
|
||||||
'–',
|
|
||||||
TorrentRendererHelper.formatDL(t),
|
|
||||||
TorrentRendererHelper.formatUL(t),
|
|
||||||
].join(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t.isSeeding()) {
|
|
||||||
return [
|
|
||||||
'Seeding to',
|
|
||||||
t.getPeersGettingFromUs(),
|
|
||||||
'of',
|
|
||||||
fmt.countString('peer', 'peers', t.getPeersConnected()),
|
|
||||||
'-',
|
|
||||||
TorrentRendererHelper.formatUL(t),
|
|
||||||
].join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t.isChecking()) {
|
|
||||||
return [
|
|
||||||
'Verifying local data (',
|
|
||||||
Transmission.fmt.percentString(100.0 * t.getRecheckProgress()),
|
|
||||||
'% tested)',
|
|
||||||
].join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.getStateString();
|
|
||||||
},
|
|
||||||
|
|
||||||
getProgressDetails: function (controller, t) {
|
|
||||||
if (t.needsMetaData()) {
|
|
||||||
let MetaDataStatus = 'retrieving';
|
|
||||||
if (t.isStopped()) {
|
|
||||||
MetaDataStatus = 'needs';
|
|
||||||
}
|
|
||||||
const percent = 100 * t.getMetadataPercentComplete();
|
|
||||||
return [
|
|
||||||
'Magnetized transfer - ' + MetaDataStatus + ' metadata (',
|
|
||||||
Transmission.fmt.percentString(percent),
|
|
||||||
'%)',
|
|
||||||
].join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
let c;
|
|
||||||
const sizeWhenDone = t.getSizeWhenDone();
|
|
||||||
const totalSize = t.getTotalSize();
|
|
||||||
const is_done = t.isDone() || t.isSeeding();
|
|
||||||
|
|
||||||
if (is_done) {
|
|
||||||
if (totalSize === sizeWhenDone) {
|
|
||||||
// seed: '698.05 MiB'
|
|
||||||
c = [Transmission.fmt.size(totalSize)];
|
|
||||||
} else {
|
|
||||||
// partial seed: '127.21 MiB of 698.05 MiB (18.2%)'
|
|
||||||
c = [
|
|
||||||
Transmission.fmt.size(sizeWhenDone),
|
|
||||||
' of ',
|
|
||||||
Transmission.fmt.size(t.getTotalSize()),
|
|
||||||
' (',
|
|
||||||
t.getPercentDoneStr(),
|
|
||||||
'%)',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
// append UL stats: ', uploaded 8.59 GiB (Ratio: 12.3)'
|
|
||||||
c.push(
|
|
||||||
', uploaded ',
|
|
||||||
Transmission.fmt.size(t.getUploadedEver()),
|
|
||||||
' (Ratio ',
|
|
||||||
Transmission.fmt.ratioString(t.getUploadRatio()),
|
|
||||||
')'
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// not done yet
|
|
||||||
c = [
|
|
||||||
Transmission.fmt.size(sizeWhenDone - t.getLeftUntilDone()),
|
|
||||||
' of ',
|
|
||||||
Transmission.fmt.size(sizeWhenDone),
|
|
||||||
' (',
|
|
||||||
t.getPercentDoneStr(),
|
|
||||||
'%)',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// maybe append eta
|
|
||||||
if (!t.isStopped() && (!is_done || t.seedRatioLimit(controller) > 0)) {
|
|
||||||
c.push(' - ');
|
|
||||||
const eta = t.getETA();
|
|
||||||
if (eta < 0 || eta >= 999 * 60 * 60 /* arbitrary */) {
|
|
||||||
c.push('remaining time unknown');
|
|
||||||
} else {
|
|
||||||
c.push(Transmission.fmt.timeInterval(t.getETA()), ' remaining');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.join('');
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function (controller, t, root) {
|
|
||||||
// name
|
|
||||||
setTextContent(root._name_container, t.getName());
|
|
||||||
|
|
||||||
// progressbar
|
|
||||||
TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
|
|
||||||
|
|
||||||
// peer details
|
|
||||||
const has_error = t.getError() !== Torrent._ErrNone;
|
|
||||||
let e = root._peer_details_container;
|
|
||||||
$(e).toggleClass('error', has_error);
|
|
||||||
setTextContent(e, this.getPeerDetails(t));
|
|
||||||
|
|
||||||
// progress details
|
|
||||||
e = root._progress_details_container;
|
|
||||||
setTextContent(e, this.getProgressDetails(controller, t));
|
|
||||||
|
|
||||||
// pause/resume button
|
|
||||||
const is_stopped = t.isStopped();
|
|
||||||
e = root._pause_resume_button_image;
|
|
||||||
e.alt = is_stopped ? 'Resume' : 'Pause';
|
|
||||||
e.className = is_stopped ? 'torrent_resume' : 'torrent_pause';
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/****
|
|
||||||
*****
|
|
||||||
*****
|
|
||||||
****/
|
|
||||||
|
|
||||||
function TorrentRendererCompact() {}
|
|
||||||
TorrentRendererCompact.prototype = {
|
|
||||||
createRow: function () {
|
|
||||||
let progressbar, details, name, root;
|
|
||||||
|
|
||||||
progressbar = TorrentRendererHelper.createProgressbar('compact');
|
|
||||||
|
|
||||||
details = document.createElement('div');
|
|
||||||
details.className = 'torrent_peer_details compact';
|
|
||||||
|
|
||||||
name = document.createElement('div');
|
|
||||||
name.className = 'torrent_name compact';
|
|
||||||
|
|
||||||
root = document.createElement('li');
|
|
||||||
root.appendChild(progressbar.element);
|
|
||||||
root.appendChild(details);
|
|
||||||
root.appendChild(name);
|
|
||||||
root.className = 'torrent compact';
|
|
||||||
root._progressbar = progressbar;
|
|
||||||
root._details_container = details;
|
|
||||||
root._name_container = name;
|
|
||||||
return root;
|
|
||||||
},
|
|
||||||
|
|
||||||
getPeerDetails: function (t) {
|
|
||||||
let c;
|
|
||||||
if ((c = t.getErrorMessage())) {
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
if (t.isDownloading()) {
|
|
||||||
const have_dn = t.getDownloadSpeed() > 0;
|
|
||||||
const have_up = t.getUploadSpeed() > 0;
|
|
||||||
|
|
||||||
if (!have_up && !have_dn) {
|
|
||||||
return 'Idle';
|
|
||||||
}
|
|
||||||
let s = '';
|
|
||||||
if (!isMobileDevice) {
|
|
||||||
s = TorrentRendererHelper.formatETA(t) + ' ';
|
|
||||||
}
|
|
||||||
if (have_dn) {
|
|
||||||
s += TorrentRendererHelper.formatDL(t);
|
|
||||||
}
|
|
||||||
if (have_dn && have_up) {
|
|
||||||
s += ' ';
|
|
||||||
}
|
|
||||||
if (have_up) {
|
|
||||||
s += TorrentRendererHelper.formatUL(t);
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
if (t.isSeeding()) {
|
|
||||||
return [
|
|
||||||
'Ratio: ',
|
|
||||||
Transmission.fmt.ratioString(t.getUploadRatio()),
|
|
||||||
', ',
|
|
||||||
TorrentRendererHelper.formatUL(t),
|
|
||||||
].join('');
|
|
||||||
}
|
|
||||||
return t.getStateString();
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function (controller, t, root) {
|
|
||||||
// name
|
|
||||||
const is_stopped = t.isStopped();
|
|
||||||
let e = root._name_container;
|
|
||||||
$(e).toggleClass('paused', is_stopped);
|
|
||||||
setTextContent(e, t.getName());
|
|
||||||
|
|
||||||
// peer details
|
|
||||||
const has_error = t.getError() !== Torrent._ErrNone;
|
|
||||||
e = root._details_container;
|
|
||||||
$(e).toggleClass('error', has_error);
|
|
||||||
setTextContent(e, this.getPeerDetails(t));
|
|
||||||
|
|
||||||
// progressbar
|
|
||||||
TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/****
|
|
||||||
*****
|
|
||||||
*****
|
|
||||||
****/
|
|
||||||
|
|
||||||
function TorrentRow(view, controller, torrent) {
|
|
||||||
this.initialize(view, controller, torrent);
|
|
||||||
}
|
|
||||||
TorrentRow.prototype = {
|
|
||||||
initialize: function (view, controller, torrent) {
|
|
||||||
const row = this;
|
|
||||||
this._view = view;
|
|
||||||
this._torrent = torrent;
|
|
||||||
this._element = view.createRow();
|
|
||||||
this.render(controller);
|
|
||||||
$(this._torrent).bind('dataChanged.torrentRowListener', function () {
|
|
||||||
row.render(controller);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getElement: function () {
|
|
||||||
return this._element;
|
|
||||||
},
|
|
||||||
render: function (controller) {
|
|
||||||
const tor = this.getTorrent();
|
|
||||||
if (tor) {
|
|
||||||
this._view.render(controller, tor, this.getElement());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isSelected: function () {
|
|
||||||
return this.getElement().className.indexOf('selected') !== -1;
|
|
||||||
},
|
|
||||||
|
|
||||||
getTorrent: function () {
|
|
||||||
return this._torrent;
|
|
||||||
},
|
|
||||||
getTorrentId: function () {
|
|
||||||
return this.getTorrent().getId();
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,614 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright © Mnemosyne LLC
|
|
||||||
*
|
|
||||||
* This file is licensed under the GPLv2.
|
|
||||||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Torrent(data) {
|
|
||||||
this.initialize(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***
|
|
||||||
****
|
|
||||||
**** Constants
|
|
||||||
****
|
|
||||||
***/
|
|
||||||
|
|
||||||
// Torrent.fields.status
|
|
||||||
Torrent._StatusStopped = 0;
|
|
||||||
Torrent._StatusCheckWait = 1;
|
|
||||||
Torrent._StatusCheck = 2;
|
|
||||||
Torrent._StatusDownloadWait = 3;
|
|
||||||
Torrent._StatusDownload = 4;
|
|
||||||
Torrent._StatusSeedWait = 5;
|
|
||||||
Torrent._StatusSeed = 6;
|
|
||||||
|
|
||||||
// Torrent.fields.seedRatioMode
|
|
||||||
Torrent._RatioUseGlobal = 0;
|
|
||||||
Torrent._RatioUseLocal = 1;
|
|
||||||
Torrent._RatioUnlimited = 2;
|
|
||||||
|
|
||||||
// Torrent.fields.error
|
|
||||||
Torrent._ErrNone = 0;
|
|
||||||
Torrent._ErrTrackerWarning = 1;
|
|
||||||
Torrent._ErrTrackerError = 2;
|
|
||||||
Torrent._ErrLocalError = 3;
|
|
||||||
|
|
||||||
// TrackerStats' announceState
|
|
||||||
Torrent._TrackerInactive = 0;
|
|
||||||
Torrent._TrackerWaiting = 1;
|
|
||||||
Torrent._TrackerQueued = 2;
|
|
||||||
Torrent._TrackerActive = 3;
|
|
||||||
|
|
||||||
Torrent.Fields = {};
|
|
||||||
|
|
||||||
// commonly used fields which only need to be loaded once,
|
|
||||||
// either on startup or when a magnet finishes downloading its metadata
|
|
||||||
// finishes downloading its metadata
|
|
||||||
Torrent.Fields.Metadata = ['addedDate', 'name', 'totalSize'];
|
|
||||||
|
|
||||||
// commonly used fields which need to be periodically refreshed
|
|
||||||
Torrent.Fields.Stats = [
|
|
||||||
'error',
|
|
||||||
'errorString',
|
|
||||||
'eta',
|
|
||||||
'isFinished',
|
|
||||||
'isStalled',
|
|
||||||
'leftUntilDone',
|
|
||||||
'metadataPercentComplete',
|
|
||||||
'peersConnected',
|
|
||||||
'peersGettingFromUs',
|
|
||||||
'peersSendingToUs',
|
|
||||||
'percentDone',
|
|
||||||
'queuePosition',
|
|
||||||
'rateDownload',
|
|
||||||
'rateUpload',
|
|
||||||
'recheckProgress',
|
|
||||||
'seedRatioMode',
|
|
||||||
'seedRatioLimit',
|
|
||||||
'sizeWhenDone',
|
|
||||||
'status',
|
|
||||||
'trackers',
|
|
||||||
'downloadDir',
|
|
||||||
'uploadedEver',
|
|
||||||
'uploadRatio',
|
|
||||||
'webseedsSendingToUs',
|
|
||||||
];
|
|
||||||
|
|
||||||
// fields used by the inspector which only need to be loaded once
|
|
||||||
Torrent.Fields.InfoExtra = [
|
|
||||||
'comment',
|
|
||||||
'creator',
|
|
||||||
'dateCreated',
|
|
||||||
'files',
|
|
||||||
'hashString',
|
|
||||||
'isPrivate',
|
|
||||||
'pieceCount',
|
|
||||||
'pieceSize',
|
|
||||||
];
|
|
||||||
|
|
||||||
// fields used in the inspector which need to be periodically refreshed
|
|
||||||
Torrent.Fields.StatsExtra = [
|
|
||||||
'activityDate',
|
|
||||||
'corruptEver',
|
|
||||||
'desiredAvailable',
|
|
||||||
'downloadedEver',
|
|
||||||
'fileStats',
|
|
||||||
'haveUnchecked',
|
|
||||||
'haveValid',
|
|
||||||
'peers',
|
|
||||||
'startDate',
|
|
||||||
'trackerStats',
|
|
||||||
];
|
|
||||||
|
|
||||||
/***
|
|
||||||
****
|
|
||||||
**** Methods
|
|
||||||
****
|
|
||||||
***/
|
|
||||||
|
|
||||||
Torrent.prototype = {
|
|
||||||
initialize: function (data) {
|
|
||||||
this.fields = {};
|
|
||||||
this.fieldObservers = {};
|
|
||||||
this.refresh(data);
|
|
||||||
},
|
|
||||||
|
|
||||||
notifyOnFieldChange: function (field, callback) {
|
|
||||||
this.fieldObservers[field] = this.fieldObservers[field] || [];
|
|
||||||
this.fieldObservers[field].push(callback);
|
|
||||||
},
|
|
||||||
|
|
||||||
setField: function (o, name, value) {
|
|
||||||
let i, observer;
|
|
||||||
|
|
||||||
if (o[name] === value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (o == this.fields && this.fieldObservers[name] && this.fieldObservers[name].length) {
|
|
||||||
for (i = 0; (observer = this.fieldObservers[name][i]); ++i) {
|
|
||||||
observer.call(this, value, o[name], name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
o[name] = value;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
// fields.files is an array of unions of RPC's "files" and "fileStats" objects.
|
|
||||||
updateFiles: function (files) {
|
|
||||||
let changed = false;
|
|
||||||
const myfiles = this.fields.files || [];
|
|
||||||
const keys = ['length', 'name', 'bytesCompleted', 'wanted', 'priority'];
|
|
||||||
let i, f, j, key, myfile;
|
|
||||||
|
|
||||||
for (i = 0; (f = files[i]); ++i) {
|
|
||||||
myfile = myfiles[i] || {};
|
|
||||||
for (j = 0; (key = keys[j]); ++j) {
|
|
||||||
if (key in f) {
|
|
||||||
changed |= this.setField(myfile, key, f[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
myfiles[i] = myfile;
|
|
||||||
}
|
|
||||||
this.fields.files = myfiles;
|
|
||||||
return changed;
|
|
||||||
},
|
|
||||||
|
|
||||||
collateTrackers: function (trackers) {
|
|
||||||
return trackers.map((t) => t.announce.toLowerCase()).join('\t');
|
|
||||||
},
|
|
||||||
|
|
||||||
refreshFields: function (data) {
|
|
||||||
let key;
|
|
||||||
let changed = false;
|
|
||||||
|
|
||||||
for (key in data) {
|
|
||||||
switch (key) {
|
|
||||||
case 'files':
|
|
||||||
case 'fileStats': // merge files and fileStats together
|
|
||||||
changed |= this.updateFiles(data[key]);
|
|
||||||
break;
|
|
||||||
case 'trackerStats': // 'trackerStats' is a superset of 'trackers'...
|
|
||||||
changed |= this.setField(this.fields, 'trackers', data[key]);
|
|
||||||
break;
|
|
||||||
case 'trackers': // ...so only save 'trackers' if we don't have it already
|
|
||||||
if (!(key in this.fields)) {
|
|
||||||
changed |= this.setField(this.fields, key, data[key]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
changed |= this.setField(this.fields, key, data[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return changed;
|
|
||||||
},
|
|
||||||
|
|
||||||
refresh: function (data) {
|
|
||||||
if (this.refreshFields(data)) {
|
|
||||||
$(this).trigger('dataChanged', this);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/****
|
|
||||||
*****
|
|
||||||
****/
|
|
||||||
|
|
||||||
// simple accessors
|
|
||||||
getComment: function () {
|
|
||||||
return this.fields.comment;
|
|
||||||
},
|
|
||||||
getCreator: function () {
|
|
||||||
return this.fields.creator;
|
|
||||||
},
|
|
||||||
getDateAdded: function () {
|
|
||||||
return this.fields.addedDate;
|
|
||||||
},
|
|
||||||
getDateCreated: function () {
|
|
||||||
return this.fields.dateCreated;
|
|
||||||
},
|
|
||||||
getDesiredAvailable: function () {
|
|
||||||
return this.fields.desiredAvailable;
|
|
||||||
},
|
|
||||||
getDownloadDir: function () {
|
|
||||||
return this.fields.downloadDir;
|
|
||||||
},
|
|
||||||
getDownloadSpeed: function () {
|
|
||||||
return this.fields.rateDownload;
|
|
||||||
},
|
|
||||||
getDownloadedEver: function () {
|
|
||||||
return this.fields.downloadedEver;
|
|
||||||
},
|
|
||||||
getError: function () {
|
|
||||||
return this.fields.error;
|
|
||||||
},
|
|
||||||
getErrorString: function () {
|
|
||||||
return this.fields.errorString;
|
|
||||||
},
|
|
||||||
getETA: function () {
|
|
||||||
return this.fields.eta;
|
|
||||||
},
|
|
||||||
getFailedEver: function (i) {
|
|
||||||
return this.fields.corruptEver;
|
|
||||||
},
|
|
||||||
getFile: function (i) {
|
|
||||||
return this.fields.files[i];
|
|
||||||
},
|
|
||||||
getFileCount: function () {
|
|
||||||
return this.fields.files ? this.fields.files.length : 0;
|
|
||||||
},
|
|
||||||
getHashString: function () {
|
|
||||||
return this.fields.hashString;
|
|
||||||
},
|
|
||||||
getHave: function () {
|
|
||||||
return this.getHaveValid() + this.getHaveUnchecked();
|
|
||||||
},
|
|
||||||
getHaveUnchecked: function () {
|
|
||||||
return this.fields.haveUnchecked;
|
|
||||||
},
|
|
||||||
getHaveValid: function () {
|
|
||||||
return this.fields.haveValid;
|
|
||||||
},
|
|
||||||
getId: function () {
|
|
||||||
return this.fields.id;
|
|
||||||
},
|
|
||||||
getLastActivity: function () {
|
|
||||||
return this.fields.activityDate;
|
|
||||||
},
|
|
||||||
getLeftUntilDone: function () {
|
|
||||||
return this.fields.leftUntilDone;
|
|
||||||
},
|
|
||||||
getMetadataPercentComplete: function () {
|
|
||||||
return this.fields.metadataPercentComplete;
|
|
||||||
},
|
|
||||||
getName: function () {
|
|
||||||
return this.fields.name || 'Unknown';
|
|
||||||
},
|
|
||||||
getPeers: function () {
|
|
||||||
return this.fields.peers;
|
|
||||||
},
|
|
||||||
getPeersConnected: function () {
|
|
||||||
return this.fields.peersConnected;
|
|
||||||
},
|
|
||||||
getPeersGettingFromUs: function () {
|
|
||||||
return this.fields.peersGettingFromUs;
|
|
||||||
},
|
|
||||||
getPeersSendingToUs: function () {
|
|
||||||
return this.fields.peersSendingToUs;
|
|
||||||
},
|
|
||||||
getPieceCount: function () {
|
|
||||||
return this.fields.pieceCount;
|
|
||||||
},
|
|
||||||
getPieceSize: function () {
|
|
||||||
return this.fields.pieceSize;
|
|
||||||
},
|
|
||||||
getPrivateFlag: function () {
|
|
||||||
return this.fields.isPrivate;
|
|
||||||
},
|
|
||||||
getQueuePosition: function () {
|
|
||||||
return this.fields.queuePosition;
|
|
||||||
},
|
|
||||||
getRecheckProgress: function () {
|
|
||||||
return this.fields.recheckProgress;
|
|
||||||
},
|
|
||||||
getSeedRatioLimit: function () {
|
|
||||||
return this.fields.seedRatioLimit;
|
|
||||||
},
|
|
||||||
getSeedRatioMode: function () {
|
|
||||||
return this.fields.seedRatioMode;
|
|
||||||
},
|
|
||||||
getSizeWhenDone: function () {
|
|
||||||
return this.fields.sizeWhenDone;
|
|
||||||
},
|
|
||||||
getStartDate: function () {
|
|
||||||
return this.fields.startDate;
|
|
||||||
},
|
|
||||||
getStatus: function () {
|
|
||||||
return this.fields.status;
|
|
||||||
},
|
|
||||||
getTotalSize: function () {
|
|
||||||
return this.fields.totalSize;
|
|
||||||
},
|
|
||||||
getTrackers: function () {
|
|
||||||
return this.fields.trackers;
|
|
||||||
},
|
|
||||||
getUploadSpeed: function () {
|
|
||||||
return this.fields.rateUpload;
|
|
||||||
},
|
|
||||||
getUploadRatio: function () {
|
|
||||||
return this.fields.uploadRatio;
|
|
||||||
},
|
|
||||||
getUploadedEver: function () {
|
|
||||||
return this.fields.uploadedEver;
|
|
||||||
},
|
|
||||||
getWebseedsSendingToUs: function () {
|
|
||||||
return this.fields.webseedsSendingToUs;
|
|
||||||
},
|
|
||||||
isFinished: function () {
|
|
||||||
return this.fields.isFinished;
|
|
||||||
},
|
|
||||||
|
|
||||||
// derived accessors
|
|
||||||
hasExtraInfo: function () {
|
|
||||||
return 'hashString' in this.fields;
|
|
||||||
},
|
|
||||||
isSeeding: function () {
|
|
||||||
return this.getStatus() === Torrent._StatusSeed;
|
|
||||||
},
|
|
||||||
isStopped: function () {
|
|
||||||
return this.getStatus() === Torrent._StatusStopped;
|
|
||||||
},
|
|
||||||
isChecking: function () {
|
|
||||||
return this.getStatus() === Torrent._StatusCheck;
|
|
||||||
},
|
|
||||||
isDownloading: function () {
|
|
||||||
return this.getStatus() === Torrent._StatusDownload;
|
|
||||||
},
|
|
||||||
isQueued: function () {
|
|
||||||
return (
|
|
||||||
this.getStatus() === Torrent._StatusDownloadWait ||
|
|
||||||
this.getStatus() === Torrent._StatusSeedWait
|
|
||||||
);
|
|
||||||
},
|
|
||||||
isDone: function () {
|
|
||||||
return this.getLeftUntilDone() < 1;
|
|
||||||
},
|
|
||||||
needsMetaData: function () {
|
|
||||||
return this.getMetadataPercentComplete() < 1;
|
|
||||||
},
|
|
||||||
getActivity: function () {
|
|
||||||
return this.getDownloadSpeed() + this.getUploadSpeed();
|
|
||||||
},
|
|
||||||
getPercentDoneStr: function () {
|
|
||||||
return Transmission.fmt.percentString(100 * this.getPercentDone());
|
|
||||||
},
|
|
||||||
getPercentDone: function () {
|
|
||||||
return this.fields.percentDone;
|
|
||||||
},
|
|
||||||
getStateString: function () {
|
|
||||||
switch (this.getStatus()) {
|
|
||||||
case Torrent._StatusStopped:
|
|
||||||
return this.isFinished() ? 'Seeding complete' : 'Paused';
|
|
||||||
case Torrent._StatusCheckWait:
|
|
||||||
return 'Queued for verification';
|
|
||||||
case Torrent._StatusCheck:
|
|
||||||
return 'Verifying local data';
|
|
||||||
case Torrent._StatusDownloadWait:
|
|
||||||
return 'Queued for download';
|
|
||||||
case Torrent._StatusDownload:
|
|
||||||
return 'Downloading';
|
|
||||||
case Torrent._StatusSeedWait:
|
|
||||||
return 'Queued for seeding';
|
|
||||||
case Torrent._StatusSeed:
|
|
||||||
return 'Seeding';
|
|
||||||
case null:
|
|
||||||
case undefined:
|
|
||||||
return 'Unknown';
|
|
||||||
default:
|
|
||||||
return 'Error';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
seedRatioLimit: function (controller) {
|
|
||||||
switch (this.getSeedRatioMode()) {
|
|
||||||
case Torrent._RatioUseGlobal:
|
|
||||||
return controller.seedRatioLimit();
|
|
||||||
case Torrent._RatioUseLocal:
|
|
||||||
return this.getSeedRatioLimit();
|
|
||||||
default:
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getErrorMessage: function () {
|
|
||||||
const str = this.getErrorString();
|
|
||||||
switch (this.getError()) {
|
|
||||||
case Torrent._ErrTrackerWarning:
|
|
||||||
return 'Tracker returned a warning: ' + str;
|
|
||||||
case Torrent._ErrTrackerError:
|
|
||||||
return 'Tracker returned an error: ' + str;
|
|
||||||
case Torrent._ErrLocalError:
|
|
||||||
return 'Error: ' + str;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getCollatedName: function () {
|
|
||||||
const f = this.fields;
|
|
||||||
if (!f.collatedName && f.name) {
|
|
||||||
f.collatedName = f.name.toLowerCase();
|
|
||||||
}
|
|
||||||
return f.collatedName || '';
|
|
||||||
},
|
|
||||||
getCollatedTrackers: function () {
|
|
||||||
const f = this.fields;
|
|
||||||
if (!f.collatedTrackers && f.trackers) {
|
|
||||||
f.collatedTrackers = this.collateTrackers(f.trackers);
|
|
||||||
}
|
|
||||||
return f.collatedTrackers || '';
|
|
||||||
},
|
|
||||||
|
|
||||||
/****
|
|
||||||
*****
|
|
||||||
****/
|
|
||||||
|
|
||||||
testState: function (state) {
|
|
||||||
const s = this.getStatus();
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case Prefs._FilterActive:
|
|
||||||
return (
|
|
||||||
this.getPeersGettingFromUs() > 0 ||
|
|
||||||
this.getPeersSendingToUs() > 0 ||
|
|
||||||
this.getWebseedsSendingToUs() > 0 ||
|
|
||||||
this.isChecking()
|
|
||||||
);
|
|
||||||
case Prefs._FilterSeeding:
|
|
||||||
return s === Torrent._StatusSeed || s === Torrent._StatusSeedWait;
|
|
||||||
case Prefs._FilterDownloading:
|
|
||||||
return s === Torrent._StatusDownload || s === Torrent._StatusDownloadWait;
|
|
||||||
case Prefs._FilterPaused:
|
|
||||||
return this.isStopped();
|
|
||||||
case Prefs._FilterFinished:
|
|
||||||
return this.isFinished();
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param filter one of Prefs._Filter*
|
|
||||||
* @param search substring to look for, or null
|
|
||||||
* @return true if it passes the test, false if it fails
|
|
||||||
*/
|
|
||||||
test: function (state, search, tracker) {
|
|
||||||
// flter by state...
|
|
||||||
let pass = this.testState(state);
|
|
||||||
|
|
||||||
// maybe filter by text...
|
|
||||||
if (pass && search && search.length) {
|
|
||||||
pass = this.getCollatedName().indexOf(search.toLowerCase()) !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// maybe filter by tracker...
|
|
||||||
if (pass && tracker && tracker.length) {
|
|
||||||
pass = this.getCollatedTrackers().indexOf(tracker) !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pass;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/***
|
|
||||||
****
|
|
||||||
**** SORTING
|
|
||||||
****
|
|
||||||
***/
|
|
||||||
|
|
||||||
Torrent.compareById = function (ta, tb) {
|
|
||||||
return ta.getId() - tb.getId();
|
|
||||||
};
|
|
||||||
Torrent.compareByName = function (ta, tb) {
|
|
||||||
return ta.getCollatedName().localeCompare(tb.getCollatedName()) || Torrent.compareById(ta, tb);
|
|
||||||
};
|
|
||||||
Torrent.compareByQueue = function (ta, tb) {
|
|
||||||
return ta.getQueuePosition() - tb.getQueuePosition();
|
|
||||||
};
|
|
||||||
Torrent.compareByAge = function (ta, tb) {
|
|
||||||
const a = ta.getDateAdded();
|
|
||||||
const b = tb.getDateAdded();
|
|
||||||
|
|
||||||
return b - a || Torrent.compareByQueue(ta, tb);
|
|
||||||
};
|
|
||||||
Torrent.compareByState = function (ta, tb) {
|
|
||||||
const a = ta.getStatus();
|
|
||||||
const b = tb.getStatus();
|
|
||||||
|
|
||||||
return b - a || Torrent.compareByQueue(ta, tb);
|
|
||||||
};
|
|
||||||
Torrent.compareByActivity = function (ta, tb) {
|
|
||||||
const a = ta.getActivity();
|
|
||||||
const b = tb.getActivity();
|
|
||||||
|
|
||||||
return b - a || Torrent.compareByState(ta, tb);
|
|
||||||
};
|
|
||||||
Torrent.compareByRatio = function (ta, tb) {
|
|
||||||
const a = ta.getUploadRatio();
|
|
||||||
const b = tb.getUploadRatio();
|
|
||||||
|
|
||||||
if (a < b) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (a > b) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return Torrent.compareByState(ta, tb);
|
|
||||||
};
|
|
||||||
Torrent.compareByProgress = function (ta, tb) {
|
|
||||||
const a = ta.getPercentDone();
|
|
||||||
const b = tb.getPercentDone();
|
|
||||||
|
|
||||||
return a - b || Torrent.compareByRatio(ta, tb);
|
|
||||||
};
|
|
||||||
Torrent.compareBySize = function (ta, tb) {
|
|
||||||
const a = ta.getTotalSize();
|
|
||||||
const b = tb.getTotalSize();
|
|
||||||
|
|
||||||
return a - b || Torrent.compareByName(ta, tb);
|
|
||||||
};
|
|
||||||
|
|
||||||
Torrent.compareTorrents = function (a, b, sortMethod, sortDirection) {
|
|
||||||
let i;
|
|
||||||
|
|
||||||
switch (sortMethod) {
|
|
||||||
case Prefs._SortByActivity:
|
|
||||||
i = Torrent.compareByActivity(a, b);
|
|
||||||
break;
|
|
||||||
case Prefs._SortByAge:
|
|
||||||
i = Torrent.compareByAge(a, b);
|
|
||||||
break;
|
|
||||||
case Prefs._SortByQueue:
|
|
||||||
i = Torrent.compareByQueue(a, b);
|
|
||||||
break;
|
|
||||||
case Prefs._SortByProgress:
|
|
||||||
i = Torrent.compareByProgress(a, b);
|
|
||||||
break;
|
|
||||||
case Prefs._SortBySize:
|
|
||||||
i = Torrent.compareBySize(a, b);
|
|
||||||
break;
|
|
||||||
case Prefs._SortByState:
|
|
||||||
i = Torrent.compareByState(a, b);
|
|
||||||
break;
|
|
||||||
case Prefs._SortByRatio:
|
|
||||||
i = Torrent.compareByRatio(a, b);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
i = Torrent.compareByName(a, b);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sortDirection === Prefs._SortDescending) {
|
|
||||||
i = -i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return i;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param torrents an array of Torrent objects
|
|
||||||
* @param sortMethod one of Prefs._SortBy*
|
|
||||||
* @param sortDirection Prefs._SortAscending or Prefs._SortDescending
|
|
||||||
*/
|
|
||||||
Torrent.sortTorrents = function (torrents, sortMethod, sortDirection) {
|
|
||||||
switch (sortMethod) {
|
|
||||||
case Prefs._SortByActivity:
|
|
||||||
torrents.sort(this.compareByActivity);
|
|
||||||
break;
|
|
||||||
case Prefs._SortByAge:
|
|
||||||
torrents.sort(this.compareByAge);
|
|
||||||
break;
|
|
||||||
case Prefs._SortByQueue:
|
|
||||||
torrents.sort(this.compareByQueue);
|
|
||||||
break;
|
|
||||||
case Prefs._SortByProgress:
|
|
||||||
torrents.sort(this.compareByProgress);
|
|
||||||
break;
|
|
||||||
case Prefs._SortBySize:
|
|
||||||
torrents.sort(this.compareBySize);
|
|
||||||
break;
|
|
||||||
case Prefs._SortByState:
|
|
||||||
torrents.sort(this.compareByState);
|
|
||||||
break;
|
|
||||||
case Prefs._SortByRatio:
|
|
||||||
torrents.sort(this.compareByRatio);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
torrents.sort(this.compareByName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sortDirection === Prefs._SortDescending) {
|
|
||||||
torrents.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
return torrents;
|
|
||||||
};
|
|
|
@ -3,60 +3,51 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": "https://github.com/transmission/transmission",
|
"repository": "https://github.com/transmission/transmission",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"babel": {
|
|
||||||
"plugins": [
|
|
||||||
"@babel/plugin-proposal-class-properties"
|
|
||||||
],
|
|
||||||
"presets": []
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true,
|
|
||||||
"jquery": true
|
|
||||||
},
|
|
||||||
"extends": "eslint:recommended",
|
|
||||||
"parser": "@babel/eslint-parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"curly": ["error", "all"],
|
|
||||||
"no-undef": "off",
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"no-var": "off",
|
|
||||||
"prefer-const": "off",
|
|
||||||
"semi": ["error", "always"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"prettier": {
|
|
||||||
"arrowParens": "always",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"embeddedLanguageFormatting": "auto",
|
|
||||||
"htmlWhitespaceSensitivity": "css",
|
|
||||||
"insertPragma": false,
|
|
||||||
"jsxBracketSameLine": false,
|
|
||||||
"jsxSingleQuote": false,
|
|
||||||
"printWidth": 100,
|
|
||||||
"proseWrap": "preserve",
|
|
||||||
"quoteProps": "as-needed",
|
|
||||||
"requirePragma": false,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"useTabs": false,
|
|
||||||
"vueIndentScriptAndStyle": false
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "prettier --loglevel warn --check javascript/*js && eslint javascript/*js",
|
"build": "webpack --config webpack.config.js",
|
||||||
"lint:fix": "prettier --loglevel warn -w javascript/*js && eslint --fix javascript/*js"
|
"css": "sass --no-source-map style/",
|
||||||
|
"css:map": "sass style/",
|
||||||
|
"lint": "run-p --silent lint:eslint lint:stylelint lint:prettier",
|
||||||
|
"lint:fix": "run-s lint:eslint:fix lint:stylelint:fix lint:prettier:fix",
|
||||||
|
"lint:eslint": "eslint src/*.js",
|
||||||
|
"lint:eslint:fix": "eslint --fix src/*.js",
|
||||||
|
"lint:prettier": "prettier --loglevel warn --check package.json public_html/index.html style/*scss src/*.js",
|
||||||
|
"lint:prettier:fix": "prettier --loglevel warn -w package.json public_html/index.html style/*scss src/*.js",
|
||||||
|
"lint:stylelint": "stylelint style/*scss",
|
||||||
|
"lint:stylelint:fix": "stylelint --fix style/*scss"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.11.6",
|
"@babel/core": "^7.11.6",
|
||||||
"@babel/eslint-parser": "^7.11.5",
|
"@babel/eslint-parser": "^7.11.5",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||||
"eslint": "^7.9.0",
|
"css-loader": "^4.3.0",
|
||||||
"prettier": "^2.1.2"
|
"eslint": "^7.11.0",
|
||||||
|
"eslint-plugin-sonarjs": "^0.5.0",
|
||||||
|
"eslint-plugin-unicorn": "^23.0.0",
|
||||||
|
"file-loader": "^6.1.0",
|
||||||
|
"img-optimize-loader": "^1.0.7",
|
||||||
|
"mini-css-extract-plugin": "^0.11.3",
|
||||||
|
"node-sass": "^4.14.1",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"optimize-css-assets-webpack-plugin": "^5.0.4",
|
||||||
|
"prettier": "^2.1.2",
|
||||||
|
"sass": "^1.26.11",
|
||||||
|
"sass-loader": "^10.0.2",
|
||||||
|
"style-loader": "^1.2.1",
|
||||||
|
"stylelint": "^13.7.1",
|
||||||
|
"stylelint-config-prettier": "^8.0.2",
|
||||||
|
"stylelint-config-primer": "^9.2.1",
|
||||||
|
"stylelint-config-sass-guidelines": "^7.1.0",
|
||||||
|
"stylelint-config-standard": "^20.0.0",
|
||||||
|
"svgo": "^1.3.2",
|
||||||
|
"svgo-loader": "^2.2.1",
|
||||||
|
"terser-webpack-plugin": "^4.2.2",
|
||||||
|
"url-loader": "^4.1.0",
|
||||||
|
"webpack": "^4.44.2",
|
||||||
|
"webpack-bundle-analyzer": "^3.9.0",
|
||||||
|
"webpack-cli": "^3.3.12"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.isequal": "^4.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
"singleQuote": true,
|
||||||
|
};
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 683 B After Width: | Height: | Size: 683 B |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
@ -0,0 +1,106 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=8,IE=9,IE=10" />
|
||||||
|
<!-- ticket #4555 -->
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||||
|
/>
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<link href="./images/favicon.ico" rel="icon" />
|
||||||
|
<link href="./images/favicon.png" rel="shortcut icon" />
|
||||||
|
<link rel="apple-touch-icon" href="./images/webclip-icon.png" />
|
||||||
|
<script type="text/javascript" src="./transmission-app.js"></script>
|
||||||
|
<title>Transmission Web Interface</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="mainwin">
|
||||||
|
<header class="mainwin-toolbar" id="mainwin-toolbar">
|
||||||
|
<button
|
||||||
|
aria-keyshortcuts="Alt+O"
|
||||||
|
aria-label="Open torrent"
|
||||||
|
class="toolbar-button"
|
||||||
|
data-action="open-torrent"
|
||||||
|
id="toolbar-open"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
aria-keyshortcuts="Alt+Delete"
|
||||||
|
aria-label="Remove selected torrents"
|
||||||
|
class="toolbar-button"
|
||||||
|
data-action="remove-selected-torrents"
|
||||||
|
href="#"
|
||||||
|
id="toolbar-remove"
|
||||||
|
></button>
|
||||||
|
<div href="#" class="toolbar-separator"></div>
|
||||||
|
<button
|
||||||
|
aria-keyshortcuts="Alt+R"
|
||||||
|
aria-label="Resume selected torrents"
|
||||||
|
class="toolbar-button"
|
||||||
|
data-action="resume-selected-torrents"
|
||||||
|
id="toolbar-start"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
aria-keyshortcuts="Alt+P"
|
||||||
|
aria-label="Pause selected torrents"
|
||||||
|
class="toolbar-button"
|
||||||
|
data-action="pause-selected-torrents"
|
||||||
|
id="toolbar-pause"
|
||||||
|
></button>
|
||||||
|
<div href="#" class="toolbar-separator"></div>
|
||||||
|
<button
|
||||||
|
aria-keyshortcuts="Alt+I"
|
||||||
|
aria-label="Toggle inspector"
|
||||||
|
class="toolbar-button"
|
||||||
|
data-action="show-inspector"
|
||||||
|
id="toolbar-inspector"
|
||||||
|
></button>
|
||||||
|
<div class="toolbar-separator"></div>
|
||||||
|
<button
|
||||||
|
aria-keyshortcuts="Alt+M"
|
||||||
|
aria-label="Show sidebar menu"
|
||||||
|
class="toolbar-button"
|
||||||
|
data-action="show-overflow-menu"
|
||||||
|
id="toolbar-overflow"
|
||||||
|
></button>
|
||||||
|
</header>
|
||||||
|
<!-- class mainwin-toolbar -->
|
||||||
|
|
||||||
|
<header class="mainwin-filterbar" id="statusbar">
|
||||||
|
<span>Show</span>
|
||||||
|
<select id="filter-mode">
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="downloading">Downloading</option>
|
||||||
|
<option value="seeding">Seeding</option>
|
||||||
|
<option value="paused">Paused</option>
|
||||||
|
<option value="finished">Finished</option>
|
||||||
|
</select>
|
||||||
|
<select id="filter-tracker"></select>
|
||||||
|
<input type="search" id="torrent-search" placeholder="Filter" />
|
||||||
|
<span id="filter-count"> </span>
|
||||||
|
<span class="flex"></span>
|
||||||
|
<div class="speed-dn-icon"></div>
|
||||||
|
<div id="speed-dn-label"></div>
|
||||||
|
<div class="speed-up-icon"></div>
|
||||||
|
<div id="speed-up-label"></div>
|
||||||
|
</header>
|
||||||
|
<!-- class mainwin-filterbar -->
|
||||||
|
|
||||||
|
<main class="mainwin-workarea">
|
||||||
|
<div id="torrent-container">
|
||||||
|
<ul class="torrent-list flex" id="torrent-list"></ul>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!-- class="mainwin-workarea" -->
|
||||||
|
|
||||||
|
<iframe
|
||||||
|
name="torrent-upload-frame"
|
||||||
|
id="torrent-upload-frame"
|
||||||
|
src="about:blank"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
<!-- class mainwin -->
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createDialogContainer } from './utils.js';
|
||||||
|
|
||||||
|
export class AboutDialog extends EventTarget {
|
||||||
|
constructor(version_info) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.elements = AboutDialog._create(version_info);
|
||||||
|
this.elements.dismiss.addEventListener('click', () => this._onDismiss());
|
||||||
|
document.body.append(this.elements.root);
|
||||||
|
this.elements.dismiss.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.elements.root.remove();
|
||||||
|
this.dispatchEvent(new Event('close'));
|
||||||
|
delete this.elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDismiss() {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static _create(version_info) {
|
||||||
|
const elements = createDialogContainer('about-dialog');
|
||||||
|
elements.root.setAttribute('aria-label', 'About transmission');
|
||||||
|
elements.heading.textContent = 'Transmission';
|
||||||
|
elements.dismiss.textContent = 'Close';
|
||||||
|
|
||||||
|
let e = document.createElement('div');
|
||||||
|
e.classList.add('about-dialog-version-number');
|
||||||
|
e.textContent = version_info.version;
|
||||||
|
elements.heading.append(e);
|
||||||
|
|
||||||
|
e = document.createElement('div');
|
||||||
|
e.classList.add('about-dialog-version-checksum');
|
||||||
|
e.textContent = version_info.checksum;
|
||||||
|
elements.heading.append(e);
|
||||||
|
|
||||||
|
e = document.createElement('div');
|
||||||
|
e.textContent = 'A fast and easy bitTorrent client';
|
||||||
|
elements.workarea.append(e);
|
||||||
|
e = document.createElement('div');
|
||||||
|
e.textContent = 'Copyright © The Transmission Project';
|
||||||
|
elements.workarea.append(e);
|
||||||
|
|
||||||
|
elements.confirm.remove();
|
||||||
|
delete elements.confirm;
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,216 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class ActionManager extends EventTarget {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.actions = Object.seal({
|
||||||
|
'deselect-all': {
|
||||||
|
enabled: false,
|
||||||
|
shortcut: 'Control+A',
|
||||||
|
text: 'Deselect all',
|
||||||
|
},
|
||||||
|
'move-bottom': { enabled: false, text: 'Move to the back of the queue' },
|
||||||
|
'move-down': { enabled: false, text: 'Move down in the queue' },
|
||||||
|
'move-top': { enabled: false, text: 'Move to the front of the queue' },
|
||||||
|
'move-up': { enabled: false, text: 'Move up in the queue' },
|
||||||
|
'open-torrent': {
|
||||||
|
enabled: true,
|
||||||
|
shortcut: 'Alt+O',
|
||||||
|
text: 'Open torrent…',
|
||||||
|
},
|
||||||
|
'pause-all-torrents': { enabled: false, text: 'Pause all' },
|
||||||
|
'pause-selected-torrents': {
|
||||||
|
enabled: false,
|
||||||
|
shortcut: 'Alt+U',
|
||||||
|
text: 'Pause',
|
||||||
|
},
|
||||||
|
'reannounce-selected-torrents': {
|
||||||
|
enabled: false,
|
||||||
|
text: 'Ask tracker for more peers',
|
||||||
|
},
|
||||||
|
'remove-selected-torrents': { enabled: false, text: 'Remove from list…' },
|
||||||
|
'resume-selected-torrents': {
|
||||||
|
enabled: false,
|
||||||
|
shortcut: 'Alt+R',
|
||||||
|
text: 'Resume',
|
||||||
|
},
|
||||||
|
'resume-selected-torrents-now': { enabled: false, text: 'Resume now' },
|
||||||
|
'select-all': { enabled: false, shortcut: 'Alt+A', text: 'Select all' },
|
||||||
|
'show-about-dialog': { enabled: true, text: 'About' },
|
||||||
|
'show-inspector': {
|
||||||
|
enabled: true,
|
||||||
|
shortcut: 'Alt+I',
|
||||||
|
text: 'Torrent Inspector',
|
||||||
|
},
|
||||||
|
'show-move-dialog': {
|
||||||
|
enabled: false,
|
||||||
|
shortcut: 'Alt+L',
|
||||||
|
text: 'Set location…',
|
||||||
|
},
|
||||||
|
'show-overflow-menu': { enabled: true, text: 'More options…' },
|
||||||
|
'show-preferences-dialog': {
|
||||||
|
enabled: true,
|
||||||
|
shortcut: 'Alt+P',
|
||||||
|
text: 'Edit preferences',
|
||||||
|
},
|
||||||
|
'show-rename-dialog': {
|
||||||
|
enabled: false,
|
||||||
|
shortcut: 'Alt+N',
|
||||||
|
text: 'Rename…',
|
||||||
|
},
|
||||||
|
'show-shortcuts-dialog': { enabled: true, text: 'Keyboard shortcuts' },
|
||||||
|
'show-statistics-dialog': {
|
||||||
|
enabled: true,
|
||||||
|
shortcut: 'Alt+S',
|
||||||
|
text: 'Statistics',
|
||||||
|
},
|
||||||
|
'start-all-torrents': { enabled: false, text: 'Start all' },
|
||||||
|
'toggle-compact-rows': { enabled: true, text: 'Compact rows' },
|
||||||
|
'trash-selected-torrents': {
|
||||||
|
enabled: false,
|
||||||
|
text: 'Trash data and remove from list…',
|
||||||
|
},
|
||||||
|
'verify-selected-torrents': {
|
||||||
|
enabled: false,
|
||||||
|
shortcut: 'Alt+V',
|
||||||
|
text: 'Verify local data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
click(name) {
|
||||||
|
if (this.isEnabled(name)) {
|
||||||
|
const event_ = new Event('click');
|
||||||
|
event_.action = name;
|
||||||
|
this.dispatchEvent(event_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getActionForShortcut(shortcut) {
|
||||||
|
for (const [name, properties] of Object.entries(this.actions)) {
|
||||||
|
if (shortcut === properties.shortcut) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return a map of shortcuts to action names
|
||||||
|
allShortcuts() {
|
||||||
|
return new Map(
|
||||||
|
Object.entries(this.actions)
|
||||||
|
.filter(([, properties]) => properties.shortcut)
|
||||||
|
.map(([name, properties]) => [properties.shortcut, name])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(name) {
|
||||||
|
return this._getAction(name).enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
text(name) {
|
||||||
|
return this._getAction(name).text;
|
||||||
|
}
|
||||||
|
|
||||||
|
keyshortcuts(name) {
|
||||||
|
return this._getAction(name).shortcut;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(event_) {
|
||||||
|
const counts = ActionManager._recount(event_.selected, event_.nonselected);
|
||||||
|
this._updateStates(counts);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getAction(name) {
|
||||||
|
const action = this.actions[name];
|
||||||
|
if (!action) {
|
||||||
|
throw new Error(`no such action: ${name}`);
|
||||||
|
}
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
static _recount(selected, nonselected) {
|
||||||
|
const test = (tor) => tor.isStopped();
|
||||||
|
const total = selected.length + nonselected.length;
|
||||||
|
const selected_paused = selected.filter(test).length;
|
||||||
|
const selected_active = selected.length - selected_paused;
|
||||||
|
const nonselected_paused = nonselected.filter(test).length;
|
||||||
|
const nonselected_active = nonselected.length - nonselected_paused;
|
||||||
|
const paused = selected_paused + nonselected_paused;
|
||||||
|
const active = selected_active + nonselected_active;
|
||||||
|
const selected_queued = selected.filter((tor) => tor.isQueued()).length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
active,
|
||||||
|
nonselected_active,
|
||||||
|
nonselected_paused,
|
||||||
|
paused,
|
||||||
|
selected: selected.length,
|
||||||
|
selected_active,
|
||||||
|
selected_paused,
|
||||||
|
selected_queued,
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateStates(counts) {
|
||||||
|
const set_enabled = (enabled, actions) => {
|
||||||
|
for (const action of actions) {
|
||||||
|
this._updateActionState(action, enabled);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
set_enabled(counts.selected_paused > 0, ['resume-selected-torrents']);
|
||||||
|
|
||||||
|
set_enabled(counts.paused > 0, ['start-all-torrents']);
|
||||||
|
|
||||||
|
set_enabled(counts.active > 0, ['pause-all-torrents']);
|
||||||
|
|
||||||
|
set_enabled(counts.selected_paused > 0 || counts.selected_queued > 0, [
|
||||||
|
'resume-selected-torrents-now',
|
||||||
|
]);
|
||||||
|
|
||||||
|
set_enabled(counts.selected_active > 0, [
|
||||||
|
'pause-selected-torrents',
|
||||||
|
'reannounce-selected-torrents',
|
||||||
|
]);
|
||||||
|
|
||||||
|
set_enabled(counts.selected > 0, [
|
||||||
|
'deselect-all',
|
||||||
|
'move-bottom',
|
||||||
|
'move-down',
|
||||||
|
'move-top',
|
||||||
|
'move-up',
|
||||||
|
'show-inspector',
|
||||||
|
'show-move-dialog',
|
||||||
|
'remove-selected-torrents',
|
||||||
|
'trash-selected-torrents',
|
||||||
|
'verify-selected-torrents',
|
||||||
|
]);
|
||||||
|
|
||||||
|
set_enabled(counts.selected === 1, ['show-rename-dialog']);
|
||||||
|
|
||||||
|
set_enabled(counts.selected < counts.total, ['select-all']);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateActionState(name, enabled) {
|
||||||
|
const action = this.actions[name];
|
||||||
|
if (!action) {
|
||||||
|
throw new Error(`no such action: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.enabled !== enabled) {
|
||||||
|
action.enabled = enabled;
|
||||||
|
|
||||||
|
const event = new Event('change');
|
||||||
|
event.action = name;
|
||||||
|
event.enabled = enabled;
|
||||||
|
this.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createDialogContainer } from './utils.js';
|
||||||
|
|
||||||
|
export class AlertDialog extends EventTarget {
|
||||||
|
constructor(options) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// options: heading, message
|
||||||
|
this.elements = AlertDialog._create(options);
|
||||||
|
this.elements.dismiss.addEventListener('click', () => this._onDismiss());
|
||||||
|
this.options = options;
|
||||||
|
document.body.append(this.elements.root);
|
||||||
|
this.elements.dismiss.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.closed) {
|
||||||
|
this.elements.root.remove();
|
||||||
|
this.dispatchEvent(new Event('close'));
|
||||||
|
for (const key of Object.keys(this)) {
|
||||||
|
delete this[key];
|
||||||
|
}
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDismiss() {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static _create(options) {
|
||||||
|
const { heading, message } = options;
|
||||||
|
const elements = createDialogContainer('confirm-dialog');
|
||||||
|
elements.confirm.remove();
|
||||||
|
delete elements.confirm;
|
||||||
|
elements.heading.textContent = heading;
|
||||||
|
elements.workarea.textContent = message;
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { setEnabled } from './utils.js';
|
||||||
|
|
||||||
|
export class ContextMenu extends EventTarget {
|
||||||
|
constructor(action_manager) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.action_listener = this._update.bind(this);
|
||||||
|
this.action_manager = action_manager;
|
||||||
|
this.action_manager.addEventListener('change', this.action_listener);
|
||||||
|
|
||||||
|
Object.assign(this, this._create());
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
for (const [action, item] of Object.entries(this.actions)) {
|
||||||
|
setEnabled(item, this.action_manager.isEnabled(action));
|
||||||
|
}
|
||||||
|
document.body.append(this.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.closed) {
|
||||||
|
this.action_manager.removeEventListener('change', this.action_listener);
|
||||||
|
this.root.remove();
|
||||||
|
this.dispatchEvent(new Event('close'));
|
||||||
|
for (const key of Object.keys(this)) {
|
||||||
|
delete this[key];
|
||||||
|
}
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_update(event_) {
|
||||||
|
const e = this.actions[event_.action];
|
||||||
|
if (e) {
|
||||||
|
setEnabled(e, event_.enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_create() {
|
||||||
|
const root = document.createElement('div');
|
||||||
|
root.role = 'menu';
|
||||||
|
root.classList.add('context-menu', 'popup');
|
||||||
|
|
||||||
|
const actions = {};
|
||||||
|
const add_item = (action) => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
const text = this.action_manager.text(action);
|
||||||
|
item.role = 'menuitem';
|
||||||
|
item.classList.add('context-menuitem');
|
||||||
|
item.dataset.action = action;
|
||||||
|
item.textContent = text;
|
||||||
|
const keyshortcuts = this.action_manager.keyshortcuts(action);
|
||||||
|
if (keyshortcuts) {
|
||||||
|
item.setAttribute('aria-keyshortcuts', keyshortcuts);
|
||||||
|
}
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
this.action_manager.click(action);
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
actions[action] = item;
|
||||||
|
root.append(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const add_separator = () => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.classList.add('context-menu-separator');
|
||||||
|
root.append(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
add_item('resume-selected-torrents');
|
||||||
|
add_item('resume-selected-torrents-now');
|
||||||
|
add_item('pause-selected-torrents');
|
||||||
|
add_separator();
|
||||||
|
add_item('move-top');
|
||||||
|
add_item('move-up');
|
||||||
|
add_item('move-down');
|
||||||
|
add_item('move-bottom');
|
||||||
|
add_separator();
|
||||||
|
add_item('remove-selected-torrents');
|
||||||
|
add_item('trash-selected-torrents');
|
||||||
|
add_separator();
|
||||||
|
add_item('verify-selected-torrents');
|
||||||
|
add_item('show-move-dialog');
|
||||||
|
add_item('show-rename-dialog');
|
||||||
|
add_separator();
|
||||||
|
add_item('reannounce-selected-torrents');
|
||||||
|
add_separator();
|
||||||
|
add_item('select-all');
|
||||||
|
add_item('deselect-all');
|
||||||
|
|
||||||
|
return { actions, root };
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Formatter } from './formatter.js';
|
||||||
|
import { makeUUID, setChecked, setEnabled, setTextContent } from './utils.js';
|
||||||
|
|
||||||
|
export class FileRow extends EventTarget {
|
||||||
|
isDone() {
|
||||||
|
return this.fields.have >= this.fields.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEditable() {
|
||||||
|
return this.fields.torrent.getFileCount() > 1 && !this.isDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshWantedHTML() {
|
||||||
|
const e = this.elements.root;
|
||||||
|
e.classList.toggle('skip', !this.fields.isWanted);
|
||||||
|
e.classList.toggle('complete', this.isDone());
|
||||||
|
setEnabled(e.checkbox, this.isEditable());
|
||||||
|
e.checkbox.checked = this.fields.isWanted;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshProgressHTML() {
|
||||||
|
const { size, have } = this.fields;
|
||||||
|
const pct = 100 * (size ? have / size : 1);
|
||||||
|
const fmt = Formatter;
|
||||||
|
const c = `${fmt.size(have)} of ${fmt.size(size)} (${fmt.percentString(
|
||||||
|
pct
|
||||||
|
)}%)`;
|
||||||
|
setTextContent(this.elements.progress, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
let have = 0;
|
||||||
|
let high = false;
|
||||||
|
let low = false;
|
||||||
|
let normal = false;
|
||||||
|
let size = 0;
|
||||||
|
let wanted = false;
|
||||||
|
|
||||||
|
// loop through the file_indices that affect this row
|
||||||
|
const files = this.fields.torrent.getFiles();
|
||||||
|
for (const index of this.fields.indices) {
|
||||||
|
const file = files[index];
|
||||||
|
have += file.bytesCompleted;
|
||||||
|
size += file.length;
|
||||||
|
wanted |= file.wanted;
|
||||||
|
switch (file.priority) {
|
||||||
|
case -1:
|
||||||
|
low = true;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
high = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
normal = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setChecked(this.elements.priority_low_button, low);
|
||||||
|
setChecked(this.elements.priority_normal_button, normal);
|
||||||
|
setChecked(this.elements.priority_high_button, high);
|
||||||
|
|
||||||
|
if (this.fields.have !== have || this.fields.size !== size) {
|
||||||
|
this.fields.have = have;
|
||||||
|
this.fields.size = size;
|
||||||
|
this.refreshProgressHTML();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fields.isWanted !== wanted) {
|
||||||
|
this.fields.isWanted = wanted;
|
||||||
|
this.refreshWantedHTML();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fireWantedChanged(wanted) {
|
||||||
|
const e = new Event('wantedToggled');
|
||||||
|
e.indices = [...this.fields.indices];
|
||||||
|
e.wanted = wanted;
|
||||||
|
this.dispatchEvent(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
firePriorityChanged(priority) {
|
||||||
|
const e = new Event('priorityToggled');
|
||||||
|
e.indices = [...this.fields.indices];
|
||||||
|
e.priority = priority;
|
||||||
|
this.dispatchEvent(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
createRow(torrent, depth, name, even) {
|
||||||
|
const root = document.createElement('li');
|
||||||
|
root.classList.add(
|
||||||
|
'inspector-torrent-file-list-entry',
|
||||||
|
even ? 'even' : 'odd'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.elements.root = root;
|
||||||
|
|
||||||
|
let e = document.createElement('input');
|
||||||
|
const check_id = makeUUID();
|
||||||
|
e.type = 'checkbox';
|
||||||
|
e.className = 'file-wanted-control';
|
||||||
|
e.title = 'Download file';
|
||||||
|
e.id = check_id;
|
||||||
|
e.addEventListener('change', (event_) =>
|
||||||
|
this.fireWantedChanged(event_.target.checked)
|
||||||
|
);
|
||||||
|
root.checkbox = e;
|
||||||
|
root.append(e);
|
||||||
|
|
||||||
|
e = document.createElement('label');
|
||||||
|
e.className = 'inspector-torrent-file-list-entry-name';
|
||||||
|
e.setAttribute('for', check_id);
|
||||||
|
setTextContent(e, name);
|
||||||
|
root.append(e);
|
||||||
|
|
||||||
|
e = document.createElement('div');
|
||||||
|
e.className = 'inspector-torrent-file-list-entry-progress';
|
||||||
|
root.append(e);
|
||||||
|
this.elements.progress = e;
|
||||||
|
|
||||||
|
e = document.createElement('div');
|
||||||
|
e.className = 'file-priority-radiobox';
|
||||||
|
const box = e;
|
||||||
|
|
||||||
|
const priority_click_listener = (event_) =>
|
||||||
|
this.firePriorityChanged(event_.target.value);
|
||||||
|
|
||||||
|
e = document.createElement('input');
|
||||||
|
e.type = 'radio';
|
||||||
|
e.value = -1;
|
||||||
|
e.className = 'low';
|
||||||
|
e.title = 'Low Priority';
|
||||||
|
e.addEventListener('click', priority_click_listener);
|
||||||
|
this.elements.priority_low_button = e;
|
||||||
|
box.append(e);
|
||||||
|
|
||||||
|
e = document.createElement('input');
|
||||||
|
e.type = 'radio';
|
||||||
|
e.value = 0;
|
||||||
|
e.className = 'normal';
|
||||||
|
e.title = 'Normal Priority';
|
||||||
|
e.addEventListener('click', priority_click_listener);
|
||||||
|
this.elements.priority_normal_button = e;
|
||||||
|
box.append(e);
|
||||||
|
|
||||||
|
e = document.createElement('input');
|
||||||
|
e.type = 'radio';
|
||||||
|
e.value = 1;
|
||||||
|
e.title = 'High Priority';
|
||||||
|
e.className = 'high';
|
||||||
|
e.addEventListener('click', priority_click_listener);
|
||||||
|
this.elements.priority_high_button = e;
|
||||||
|
box.append(e);
|
||||||
|
|
||||||
|
root.append(box);
|
||||||
|
|
||||||
|
root.style.paddingLeft = `${depth * 20}px`;
|
||||||
|
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PUBLIC
|
||||||
|
|
||||||
|
getElement() {
|
||||||
|
return this.elements.root;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(torrent, depth, name, indices, even) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.fields = {
|
||||||
|
have: 0,
|
||||||
|
indices,
|
||||||
|
isWanted: true,
|
||||||
|
// priorityHigh: false,
|
||||||
|
// priorityLow: false,
|
||||||
|
// priorityNormal: false,
|
||||||
|
size: 0,
|
||||||
|
torrent,
|
||||||
|
};
|
||||||
|
this.elements = {
|
||||||
|
priority_high_button: null,
|
||||||
|
priority_low_button: null,
|
||||||
|
priority_normal_button: null,
|
||||||
|
progress: null,
|
||||||
|
root: null,
|
||||||
|
};
|
||||||
|
this.createRow(torrent, depth, name, even);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const plural_rules = new Intl.PluralRules();
|
||||||
|
const current_locale = plural_rules.resolvedOptions().locale;
|
||||||
|
const number_format = new Intl.NumberFormat(current_locale);
|
||||||
|
|
||||||
|
const kilo = 1000;
|
||||||
|
const mem_formatters = [
|
||||||
|
new Intl.NumberFormat(current_locale, { style: 'unit', unit: 'byte' }),
|
||||||
|
new Intl.NumberFormat(current_locale, { style: 'unit', unit: 'kilobyte' }),
|
||||||
|
new Intl.NumberFormat(current_locale, { style: 'unit', unit: 'megabyte' }),
|
||||||
|
new Intl.NumberFormat(current_locale, { style: 'unit', unit: 'gigabyte' }),
|
||||||
|
new Intl.NumberFormat(current_locale, { style: 'unit', unit: 'terabyte' }),
|
||||||
|
new Intl.NumberFormat(current_locale, { style: 'unit', unit: 'petabyte' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const fmt_kBps = new Intl.NumberFormat(current_locale, {
|
||||||
|
style: 'unit',
|
||||||
|
unit: 'kilobyte-per-second',
|
||||||
|
});
|
||||||
|
const fmt_MBps = new Intl.NumberFormat(current_locale, {
|
||||||
|
style: 'unit',
|
||||||
|
unit: 'megabyte-per-second',
|
||||||
|
});
|
||||||
|
|
||||||
|
export class Formatter {
|
||||||
|
static countString(msgid, msgid_plural, n) {
|
||||||
|
return `${this.number(n)} ${this.ngettext(msgid, msgid_plural, n)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formats the a memory size into a human-readable string
|
||||||
|
// @param {Number} bytes the filesize in bytes
|
||||||
|
// @return {String} human-readable string
|
||||||
|
static mem(bytes) {
|
||||||
|
if (bytes < 0) {
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
if (bytes === 0) {
|
||||||
|
return 'None';
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = bytes;
|
||||||
|
for (const nf of mem_formatters) {
|
||||||
|
if (size < kilo) {
|
||||||
|
return nf.format(Math.floor(size));
|
||||||
|
}
|
||||||
|
size /= kilo;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'E2BIG';
|
||||||
|
}
|
||||||
|
|
||||||
|
static ngettext(msgid, msgid_plural, n) {
|
||||||
|
return plural_rules.select(n) === 'one' ? msgid : msgid_plural;
|
||||||
|
}
|
||||||
|
|
||||||
|
// format a percentage to a string
|
||||||
|
static percentString(x) {
|
||||||
|
const decimal_places = x < 100 ? 1 : 0;
|
||||||
|
return this._toTruncFixed(x, decimal_places);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Format a ratio to a string
|
||||||
|
*/
|
||||||
|
static ratioString(x) {
|
||||||
|
if (x === -1) {
|
||||||
|
return 'None';
|
||||||
|
}
|
||||||
|
if (x === -2) {
|
||||||
|
return '∞';
|
||||||
|
}
|
||||||
|
return this.percentString(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the a disk capacity or file size into a human-readable string
|
||||||
|
* @param {Number} bytes the filesize in bytes
|
||||||
|
* @return {String} human-readable string
|
||||||
|
*/
|
||||||
|
static size(bytes) {
|
||||||
|
return this.mem(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static speed(KBps) {
|
||||||
|
return KBps < 999.95 ? fmt_kBps.format(KBps) : fmt_MBps.format(KBps / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static speedBps(Bps) {
|
||||||
|
return this.speed(this.toKBps(Bps));
|
||||||
|
}
|
||||||
|
|
||||||
|
static timeInterval(seconds) {
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
if (days) {
|
||||||
|
return this.countString('day', 'days', days);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
if (hours) {
|
||||||
|
return this.countString('hour', 'hours', hours);
|
||||||
|
}
|
||||||
|
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
if (minutes) {
|
||||||
|
return this.countString('minute', 'minutes', minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
seconds = Math.floor(seconds % 60);
|
||||||
|
return this.countString('second', 'seconds', seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
static timestamp(seconds) {
|
||||||
|
if (!seconds) {
|
||||||
|
return 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
const myDate = new Date(seconds * 1000);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
let date = '';
|
||||||
|
let time = '';
|
||||||
|
|
||||||
|
const sameYear = now.getFullYear() === myDate.getFullYear();
|
||||||
|
const sameMonth = now.getMonth() === myDate.getMonth();
|
||||||
|
|
||||||
|
const dateDiff = now.getDate() - myDate.getDate();
|
||||||
|
if (sameYear && sameMonth && Math.abs(dateDiff) <= 1) {
|
||||||
|
if (dateDiff === 0) {
|
||||||
|
date = 'Today';
|
||||||
|
} else if (dateDiff === 1) {
|
||||||
|
date = 'Yesterday';
|
||||||
|
} else {
|
||||||
|
date = 'Tomorrow';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
date = myDate.toDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
let hours = myDate.getHours();
|
||||||
|
let period = 'AM';
|
||||||
|
if (hours > 12) {
|
||||||
|
hours = hours - 12;
|
||||||
|
period = 'PM';
|
||||||
|
}
|
||||||
|
if (hours === 0) {
|
||||||
|
hours = 12;
|
||||||
|
}
|
||||||
|
if (hours < 10) {
|
||||||
|
hours = `0${hours}`;
|
||||||
|
}
|
||||||
|
let minutes = myDate.getMinutes();
|
||||||
|
if (minutes < 10) {
|
||||||
|
minutes = `0${minutes}`;
|
||||||
|
}
|
||||||
|
seconds = myDate.getSeconds();
|
||||||
|
if (seconds < 10) {
|
||||||
|
seconds = `0${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
time = [hours, minutes, seconds].join(':');
|
||||||
|
|
||||||
|
return [date, time, period].join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
static toKBps(Bps) {
|
||||||
|
return Math.floor(Bps / kilo);
|
||||||
|
}
|
||||||
|
|
||||||
|
static number(number) {
|
||||||
|
return number_format.format(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Round a string of a number to a specified number of decimal places */
|
||||||
|
static _toTruncFixed(number, places) {
|
||||||
|
const returnValue = Math.floor(number * 10 ** places) / 10 ** places;
|
||||||
|
return returnValue.toFixed(places);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,927 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FileRow } from './file-row.js';
|
||||||
|
import { Formatter } from './formatter.js';
|
||||||
|
import { Torrent } from './torrent.js';
|
||||||
|
import {
|
||||||
|
OutsideClickListener,
|
||||||
|
Utils,
|
||||||
|
createTabsContainer,
|
||||||
|
setTextContent,
|
||||||
|
} from './utils.js';
|
||||||
|
|
||||||
|
const peer_column_classes = [
|
||||||
|
'encryption',
|
||||||
|
'speed-up',
|
||||||
|
'speed-down',
|
||||||
|
'percent-done',
|
||||||
|
'status',
|
||||||
|
'peer-address',
|
||||||
|
'peer-app-name',
|
||||||
|
];
|
||||||
|
|
||||||
|
export class Inspector extends EventTarget {
|
||||||
|
constructor(controller) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.closed = false;
|
||||||
|
this.controller = controller;
|
||||||
|
this.elements = this._create();
|
||||||
|
this.current_page = this.elements.info.root;
|
||||||
|
this.interval = setInterval(this._refreshTorrents.bind(this), 3000);
|
||||||
|
this.name = 'inspector';
|
||||||
|
this.selection_listener = (event_) => this._setTorrents(event_.selected);
|
||||||
|
this.torrent_listener = () => this._updateCurrentPage();
|
||||||
|
this.torrents = [];
|
||||||
|
this.file_torrent = null;
|
||||||
|
this.file_torrent_n = null;
|
||||||
|
this.file_rows = null;
|
||||||
|
this.outside = new OutsideClickListener(this.elements.root);
|
||||||
|
this.outside.addEventListener('click', () => this.close());
|
||||||
|
Object.seal(this);
|
||||||
|
|
||||||
|
controller.addEventListener(
|
||||||
|
'torrent-selection-changed',
|
||||||
|
this.selection_listener
|
||||||
|
);
|
||||||
|
this._setTorrents(this.controller.getSelectedTorrents());
|
||||||
|
|
||||||
|
document.body.append(this.elements.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.closed) {
|
||||||
|
this.outside.stop();
|
||||||
|
clearInterval(this.interval);
|
||||||
|
this._setTorrents([]);
|
||||||
|
this.elements.root.remove();
|
||||||
|
this.controller.removeEventListener(
|
||||||
|
'torrent-selection-changed',
|
||||||
|
this.selection_listener
|
||||||
|
);
|
||||||
|
this.dispatchEvent(new Event('close'));
|
||||||
|
for (const property of Object.keys(this)) {
|
||||||
|
this[property] = null;
|
||||||
|
}
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static _createInfoPage() {
|
||||||
|
const root = document.createElement('div');
|
||||||
|
root.classList.add('inspector-info-page');
|
||||||
|
const elements = { root };
|
||||||
|
|
||||||
|
const append_section_title = (text) => {
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.textContent = text;
|
||||||
|
label.classList.add('section-label');
|
||||||
|
root.append(label);
|
||||||
|
};
|
||||||
|
|
||||||
|
const append_row = (text) => {
|
||||||
|
const lhs = document.createElement('label');
|
||||||
|
setTextContent(lhs, text);
|
||||||
|
root.append(lhs);
|
||||||
|
|
||||||
|
const rhs = document.createElement('label');
|
||||||
|
root.append(rhs);
|
||||||
|
return rhs;
|
||||||
|
};
|
||||||
|
|
||||||
|
append_section_title('Activity');
|
||||||
|
let rows = [
|
||||||
|
['have', 'Have:'],
|
||||||
|
['availability', 'Availability:'],
|
||||||
|
['uploaded', 'Uploaded:'],
|
||||||
|
['downloaded', 'Downloaded:'],
|
||||||
|
['state', 'State:'],
|
||||||
|
['running_time', 'Running time:'],
|
||||||
|
['remaining_time', 'Remaining:'],
|
||||||
|
['last_activity', 'Last activity:'],
|
||||||
|
['error', 'Error:'],
|
||||||
|
];
|
||||||
|
for (const [name, text] of rows) {
|
||||||
|
elements[name] = append_row(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
append_section_title('Details');
|
||||||
|
rows = [
|
||||||
|
['size', 'Size:'],
|
||||||
|
['location', 'Location:'],
|
||||||
|
['hash', 'Hash:'],
|
||||||
|
['privacy', 'Privacy:'],
|
||||||
|
['origin', 'Origin:'],
|
||||||
|
['comment', 'Comment:'],
|
||||||
|
];
|
||||||
|
for (const [name, text] of rows) {
|
||||||
|
elements[name] = append_row(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
static _createListPage(list_type, list_id) {
|
||||||
|
const root = document.createElement('div');
|
||||||
|
const list = document.createElement(list_type);
|
||||||
|
list.id = list_id;
|
||||||
|
root.append(list);
|
||||||
|
return { list, root };
|
||||||
|
}
|
||||||
|
|
||||||
|
static _createTiersPage() {
|
||||||
|
return Inspector._createListPage('div', 'inspector-tiers-list');
|
||||||
|
}
|
||||||
|
|
||||||
|
static _createFilesPage() {
|
||||||
|
return Inspector._createListPage('ul', 'inspector-file-list');
|
||||||
|
}
|
||||||
|
|
||||||
|
static _createPeersPage() {
|
||||||
|
const table = document.createElement('table');
|
||||||
|
table.classList.add('peer-list');
|
||||||
|
const thead = document.createElement('thead');
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
const names = ['', 'Up', 'Down', 'Done', 'Status', 'Address', 'Client'];
|
||||||
|
names.forEach((name, index) => {
|
||||||
|
const th = document.createElement('th');
|
||||||
|
const classname = peer_column_classes[index];
|
||||||
|
if (classname === 'encryption') {
|
||||||
|
th.dataset.encrypted = true;
|
||||||
|
}
|
||||||
|
th.classList.add(classname);
|
||||||
|
setTextContent(th, name);
|
||||||
|
tr.append(th);
|
||||||
|
});
|
||||||
|
const tbody = document.createElement('tbody');
|
||||||
|
thead.append(tr);
|
||||||
|
table.append(thead);
|
||||||
|
table.append(tbody);
|
||||||
|
return {
|
||||||
|
root: table,
|
||||||
|
tbody,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_create() {
|
||||||
|
const pages = {
|
||||||
|
files: Inspector._createFilesPage(),
|
||||||
|
info: Inspector._createInfoPage(),
|
||||||
|
peers: Inspector._createPeersPage(),
|
||||||
|
tiers: Inspector._createTiersPage(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const on_activated = (page) => {
|
||||||
|
this.current_page = page;
|
||||||
|
this._updateCurrentPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const elements = createTabsContainer(
|
||||||
|
'inspector',
|
||||||
|
[
|
||||||
|
['inspector-tab-info', pages.info.root],
|
||||||
|
['inspector-tab-peers', pages.peers.root],
|
||||||
|
['inspector-tab-tiers', pages.tiers.root],
|
||||||
|
['inspector-tab-files', pages.files.root],
|
||||||
|
],
|
||||||
|
on_activated.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...elements, ...pages };
|
||||||
|
}
|
||||||
|
|
||||||
|
_setTorrents(torrents) {
|
||||||
|
// update the inspector when a selected torrent's data changes.
|
||||||
|
const key = 'dataChanged';
|
||||||
|
const callback = this.torrent_listener;
|
||||||
|
this.torrents.forEach((t) => t.removeEventListener(key, callback));
|
||||||
|
this.torrents = [...torrents];
|
||||||
|
this.torrents.forEach((t) => t.addEventListener(key, callback));
|
||||||
|
|
||||||
|
this._refreshTorrents();
|
||||||
|
this._updateCurrentPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
static _needsExtraInfo(torrents) {
|
||||||
|
return torrents.some((tor) => !tor.hasExtraInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
_refreshTorrents() {
|
||||||
|
const { controller, torrents } = this;
|
||||||
|
const ids = torrents.map((t) => t.getId());
|
||||||
|
|
||||||
|
if (ids && ids.length > 0) {
|
||||||
|
const fields = ['id', ...Torrent.Fields.StatsExtra];
|
||||||
|
if (Inspector._needsExtraInfo(torrents)) {
|
||||||
|
fields.push(...Torrent.Fields.InfoExtra);
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.updateTorrents(ids, fields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateCurrentPage() {
|
||||||
|
const { elements } = this;
|
||||||
|
switch (this.current_page) {
|
||||||
|
case elements.files.root:
|
||||||
|
this._updateFiles();
|
||||||
|
break;
|
||||||
|
case elements.info.root:
|
||||||
|
this._updateInfo();
|
||||||
|
break;
|
||||||
|
case elements.peers.root:
|
||||||
|
this._updatePeers();
|
||||||
|
break;
|
||||||
|
case elements.tiers.root:
|
||||||
|
this._updateTiers();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn('unexpected page');
|
||||||
|
console.log(this.current_page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateInfo() {
|
||||||
|
const none = 'None';
|
||||||
|
const mixed = 'Mixed';
|
||||||
|
const unknown = 'Unknown';
|
||||||
|
const fmt = Formatter;
|
||||||
|
const now = Date.now();
|
||||||
|
const { torrents } = this;
|
||||||
|
const e = this.elements;
|
||||||
|
const sizeWhenDone = torrents.reduce(
|
||||||
|
(accumulator, t) => accumulator + t.getSizeWhenDone(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// state
|
||||||
|
let string = null;
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else if (torrents.every((t) => t.isFinished())) {
|
||||||
|
string = 'Finished';
|
||||||
|
} else if (torrents.every((t) => t.isStopped())) {
|
||||||
|
string = 'Paused';
|
||||||
|
} else {
|
||||||
|
const get = (t) => t.getStateString();
|
||||||
|
const first = get(torrents[0]);
|
||||||
|
string = torrents.every((t) => get(t) === first) ? first : mixed;
|
||||||
|
}
|
||||||
|
setTextContent(e.info.state, string);
|
||||||
|
const stateString = string;
|
||||||
|
|
||||||
|
// have
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else {
|
||||||
|
const verified = torrents.reduce(
|
||||||
|
(accumulator, t) => accumulator + t.getHaveValid(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const unverified = torrents.reduce(
|
||||||
|
(accumulator, t) => accumulator + t.getHaveUnchecked(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const leftUntilDone = torrents.reduce(
|
||||||
|
(accumulator, t) => accumulator + t.getLeftUntilDone(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const d =
|
||||||
|
100 *
|
||||||
|
(sizeWhenDone ? (sizeWhenDone - leftUntilDone) / sizeWhenDone : 1);
|
||||||
|
string = fmt.percentString(d);
|
||||||
|
|
||||||
|
if (!unverified && !leftUntilDone) {
|
||||||
|
string = `${fmt.size(verified)} (100%)`;
|
||||||
|
} else if (!unverified) {
|
||||||
|
string = `${fmt.size(verified)} of ${fmt.size(
|
||||||
|
sizeWhenDone
|
||||||
|
)} (${string}%)`;
|
||||||
|
} else {
|
||||||
|
string = `${fmt.size(verified)} of ${fmt.size(
|
||||||
|
sizeWhenDone
|
||||||
|
)} (${string}%), ${fmt.size(unverified)} Unverified`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTextContent(e.info.have, string);
|
||||||
|
|
||||||
|
// availability
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else if (sizeWhenDone === 0) {
|
||||||
|
string = none;
|
||||||
|
} else {
|
||||||
|
const available = torrents.reduce(
|
||||||
|
(accumulator, t) => t.getHave() + t.getDesiredAvailable(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
string = `${fmt.percentString((100 * available) / sizeWhenDone)}%`;
|
||||||
|
}
|
||||||
|
setTextContent(e.info.availability, string);
|
||||||
|
|
||||||
|
// downloaded
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else {
|
||||||
|
const d = torrents.reduce(
|
||||||
|
(accumulator, t) => accumulator + t.getDownloadedEver(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const f = torrents.reduce(
|
||||||
|
(accumulator, t) => accumulator + t.getFailedEver(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
string = f ? `${fmt.size(d)} (${fmt.size(f)} corrupt)` : fmt.size(d);
|
||||||
|
}
|
||||||
|
setTextContent(e.info.downloaded, string);
|
||||||
|
|
||||||
|
// uploaded
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else {
|
||||||
|
const u = torrents.reduce(
|
||||||
|
(accumulator, t) => accumulator + t.getUploadedEver(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const d =
|
||||||
|
torrents.reduce(
|
||||||
|
(accumulator, t) => accumulator + t.getDownloadedEver(),
|
||||||
|
0
|
||||||
|
) ||
|
||||||
|
torrents.reduce((accumulator, t) => accumulator + t.getHaveValid(), 0);
|
||||||
|
string = `${fmt.size(u)} (Ratio: ${fmt.ratioString(Utils.ratio(u, d))})`;
|
||||||
|
}
|
||||||
|
setTextContent(e.info.uploaded, string);
|
||||||
|
|
||||||
|
// running time
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else if (torrents.every((t) => t.isStopped())) {
|
||||||
|
string = stateString; // paused || finished}
|
||||||
|
} else {
|
||||||
|
const get = (t) => t.getStartDate();
|
||||||
|
const first = get(torrents[0]);
|
||||||
|
string = !torrents.every((t) => get(t) === first)
|
||||||
|
? mixed
|
||||||
|
: fmt.timeInterval(now / 1000 - first);
|
||||||
|
}
|
||||||
|
setTextContent(e.info.running_time, string);
|
||||||
|
|
||||||
|
// remaining time
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else {
|
||||||
|
const get = (t) => t.getETA();
|
||||||
|
const first = get(torrents[0]);
|
||||||
|
if (!torrents.every((t) => get(t) === first)) {
|
||||||
|
string = mixed;
|
||||||
|
} else if (first < 0) {
|
||||||
|
string = unknown;
|
||||||
|
} else {
|
||||||
|
string = fmt.timeInterval(first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTextContent(e.info.remaining_time, string);
|
||||||
|
|
||||||
|
// last active at
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else {
|
||||||
|
const latest = torrents.reduce(
|
||||||
|
(accumulator, t) => Math.max(accumulator, t.getLastActivity()),
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
const now_seconds = Math.floor(now / 1000);
|
||||||
|
if (0 < latest && latest <= now_seconds) {
|
||||||
|
const idle_secs = now_seconds - latest;
|
||||||
|
string =
|
||||||
|
idle_secs < 5 ? 'Active now' : `${fmt.timeInterval(idle_secs)} ago`;
|
||||||
|
} else {
|
||||||
|
string = none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTextContent(e.info.last_activity, string);
|
||||||
|
|
||||||
|
// error
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else {
|
||||||
|
const get = (t) => t.getErrorString();
|
||||||
|
const first = get(torrents[0]);
|
||||||
|
string = torrents.every((t) => get(t) === first) ? first : mixed;
|
||||||
|
}
|
||||||
|
setTextContent(e.info.error, string || none);
|
||||||
|
|
||||||
|
// size
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else {
|
||||||
|
const size = torrents.reduce(
|
||||||
|
(accumulator, t) => accumulator + t.getTotalSize(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
if (!size) {
|
||||||
|
string = 'None';
|
||||||
|
} else {
|
||||||
|
const get = (t) => t.getPieceSize();
|
||||||
|
const pieceCount = torrents.reduce(
|
||||||
|
(accumulator, t) => accumulator + t.getPieceCount(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const pieceString = fmt.number(pieceCount);
|
||||||
|
const pieceSize = get(torrents[0]);
|
||||||
|
string = torrents.every((t) => get(t) === pieceSize)
|
||||||
|
? `${fmt.size(size)} (${pieceString} pieces @ ${fmt.mem(pieceSize)})`
|
||||||
|
: `${fmt.size(size)} (${pieceString} pieces)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTextContent(e.info.size, string);
|
||||||
|
|
||||||
|
// hash
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else {
|
||||||
|
const get = (t) => t.getHashString();
|
||||||
|
const first = get(torrents[0]);
|
||||||
|
string = torrents.every((t) => get(t) === first) ? first : mixed;
|
||||||
|
}
|
||||||
|
setTextContent(e.info.hash, string);
|
||||||
|
|
||||||
|
// privacy
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else {
|
||||||
|
const get = (t) => t.getPrivateFlag();
|
||||||
|
const first = get(torrents[0]);
|
||||||
|
if (!torrents.every((t) => get(t) === first)) {
|
||||||
|
string = mixed;
|
||||||
|
} else if (first) {
|
||||||
|
string = 'Private to this tracker -- DHT and PEX disabled';
|
||||||
|
} else {
|
||||||
|
string = 'Public torrent';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTextContent(e.info.privacy, string);
|
||||||
|
|
||||||
|
// comment
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else {
|
||||||
|
const get = (t) => t.getComment();
|
||||||
|
const first = get(torrents[0]);
|
||||||
|
string = torrents.every((t) => get(t) === first) ? first : mixed;
|
||||||
|
}
|
||||||
|
string = string || none;
|
||||||
|
if (string.startsWith('https://') || string.startsWith('http://')) {
|
||||||
|
string = encodeURI(string);
|
||||||
|
Utils.setInnerHTML(
|
||||||
|
e.info.comment,
|
||||||
|
`<a href="${string}" target="_blank" >${string}</a>`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setTextContent(e.info.comment, string);
|
||||||
|
}
|
||||||
|
|
||||||
|
// origin
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else {
|
||||||
|
let get = (t) => t.getCreator();
|
||||||
|
const creator = get(torrents[0]);
|
||||||
|
const mixed_creator = !torrents.every((t) => get(t) === creator);
|
||||||
|
|
||||||
|
get = (t) => t.getDateCreated();
|
||||||
|
const date = get(torrents[0]);
|
||||||
|
const mixed_date = !torrents.every((t) => get(t) === date);
|
||||||
|
|
||||||
|
const empty_creator = !creator || !creator.length;
|
||||||
|
const empty_date = !date;
|
||||||
|
if (mixed_creator || mixed_date) {
|
||||||
|
string = mixed;
|
||||||
|
} else if (empty_creator && empty_date) {
|
||||||
|
string = unknown;
|
||||||
|
} else if (empty_date && !empty_creator) {
|
||||||
|
string = `Created by ${creator}`;
|
||||||
|
} else if (empty_creator && !empty_date) {
|
||||||
|
string = `Created on ${new Date(date * 1000).toDateString()}`;
|
||||||
|
} else {
|
||||||
|
string = `Created by ${creator} on ${new Date(
|
||||||
|
date * 1000
|
||||||
|
).toDateString()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTextContent(e.info.origin, string);
|
||||||
|
|
||||||
|
// location
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
string = none;
|
||||||
|
} else {
|
||||||
|
const get = (t) => t.getDownloadDir();
|
||||||
|
const first = get(torrents[0]);
|
||||||
|
string = torrents.every((t) => get(t) === first) ? first : mixed;
|
||||||
|
}
|
||||||
|
setTextContent(e.info.location, string);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PEERS PAGE
|
||||||
|
|
||||||
|
static _peerStatusTitle(flag_string) {
|
||||||
|
const texts = Object.seal({
|
||||||
|
'?': "We unchoked this peer, but they're not interested",
|
||||||
|
D: 'Downloading from this peer',
|
||||||
|
E: 'Encrypted Connection',
|
||||||
|
H: 'Peer was discovered through Distributed Hash Table (DHT)',
|
||||||
|
I: 'Peer is an incoming connection',
|
||||||
|
K: "Peer has unchoked us, but we're not interested",
|
||||||
|
O: 'Optimistic unchoke',
|
||||||
|
T: 'Peer is connected via uTP',
|
||||||
|
U: 'Uploading to peer',
|
||||||
|
X: 'Peer was discovered through Peer Exchange (PEX)',
|
||||||
|
d: "We would download from this peer if they'd let us",
|
||||||
|
u: "We would upload to this peer if they'd ask",
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...flag_string]
|
||||||
|
.filter((ch) => texts[ch])
|
||||||
|
.map((ch) => `${ch}: ${texts[ch]}`)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
_updatePeers() {
|
||||||
|
const fmt = Formatter;
|
||||||
|
const { torrents } = this;
|
||||||
|
const { tbody } = this.elements.peers;
|
||||||
|
|
||||||
|
const cell_setters = [
|
||||||
|
(peer, td) => {
|
||||||
|
td.dataset.encrypted = peer.isEncrypted;
|
||||||
|
},
|
||||||
|
(peer, td) =>
|
||||||
|
setTextContent(
|
||||||
|
td,
|
||||||
|
peer.rateToPeer ? fmt.speedBps(peer.rateToPeer) : ''
|
||||||
|
),
|
||||||
|
(peer, td) =>
|
||||||
|
setTextContent(
|
||||||
|
td,
|
||||||
|
peer.rateToClient ? fmt.speedBps(peer.rateToClient) : ''
|
||||||
|
),
|
||||||
|
(peer, td) => setTextContent(td, `${Math.floor(peer.progress * 100)}%`),
|
||||||
|
(peer, td) => {
|
||||||
|
setTextContent(td, peer.flagStr);
|
||||||
|
td.setAttribute('title', Inspector._peerStatusTitle(peer.flagStr));
|
||||||
|
},
|
||||||
|
(peer, td) => setTextContent(td, peer.address),
|
||||||
|
(peer, td) => setTextContent(td, peer.clientName),
|
||||||
|
];
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (const tor of torrents) {
|
||||||
|
// torrent name
|
||||||
|
const tortr = document.createElement('tr');
|
||||||
|
tortr.classList.add('torrent-row');
|
||||||
|
const tortd = document.createElement('td');
|
||||||
|
tortd.setAttribute('colspan', cell_setters.length);
|
||||||
|
setTextContent(tortd, tor.getName());
|
||||||
|
tortr.append(tortd);
|
||||||
|
rows.push(tortr);
|
||||||
|
|
||||||
|
// peers
|
||||||
|
for (const peer of tor.getPeers()) {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.classList.add('peer-row');
|
||||||
|
cell_setters.forEach((setter, index) => {
|
||||||
|
const td = document.createElement('td');
|
||||||
|
td.classList.add(peer_column_classes[index]);
|
||||||
|
setter(peer, td);
|
||||||
|
tr.append(td);
|
||||||
|
});
|
||||||
|
rows.push(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: modify instead of rebuilding wholesale?
|
||||||
|
while (tbody.firstChild) {
|
||||||
|
tbody.firstChild.remove();
|
||||||
|
}
|
||||||
|
tbody.append(...rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACKERS PAGE
|
||||||
|
|
||||||
|
static getAnnounceState(tracker) {
|
||||||
|
switch (tracker.announceState) {
|
||||||
|
case Torrent._TrackerActive:
|
||||||
|
return 'Announce in progress';
|
||||||
|
case Torrent._TrackerWaiting: {
|
||||||
|
const timeUntilAnnounce = Math.max(
|
||||||
|
0,
|
||||||
|
tracker.nextAnnounceTime - new Date().getTime() / 1000
|
||||||
|
);
|
||||||
|
return `Next announce in ${Formatter.timeInterval(timeUntilAnnounce)}`;
|
||||||
|
}
|
||||||
|
case Torrent._TrackerQueued:
|
||||||
|
return 'Announce is queued';
|
||||||
|
case Torrent._TrackerInactive:
|
||||||
|
return tracker.isBackup
|
||||||
|
? 'Tracker will be used as a backup'
|
||||||
|
: 'Announce not scheduled';
|
||||||
|
default:
|
||||||
|
return `unknown announce state: ${tracker.announceState}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static lastAnnounceStatus(tracker) {
|
||||||
|
let lastAnnounceLabel = 'Last Announce';
|
||||||
|
let lastAnnounce = ['N/A'];
|
||||||
|
|
||||||
|
if (tracker.hasAnnounced) {
|
||||||
|
const lastAnnounceTime = Formatter.timestamp(tracker.lastAnnounceTime);
|
||||||
|
if (tracker.lastAnnounceSucceeded) {
|
||||||
|
lastAnnounce = [
|
||||||
|
lastAnnounceTime,
|
||||||
|
' (got ',
|
||||||
|
Formatter.countString('peer', 'peers', tracker.lastAnnouncePeerCount),
|
||||||
|
')',
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
lastAnnounceLabel = 'Announce error';
|
||||||
|
lastAnnounce = [
|
||||||
|
tracker.lastAnnounceResult ? `${tracker.lastAnnounceResult} - ` : '',
|
||||||
|
lastAnnounceTime,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: lastAnnounceLabel,
|
||||||
|
value: lastAnnounce.join(''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static lastScrapeStatus(tracker) {
|
||||||
|
let lastScrapeLabel = 'Last Scrape';
|
||||||
|
let lastScrape = 'N/A';
|
||||||
|
|
||||||
|
if (tracker.hasScraped) {
|
||||||
|
const lastScrapeTime = Formatter.timestamp(tracker.lastScrapeTime);
|
||||||
|
if (tracker.lastScrapeSucceeded) {
|
||||||
|
lastScrape = lastScrapeTime;
|
||||||
|
} else {
|
||||||
|
lastScrapeLabel = 'Scrape error';
|
||||||
|
lastScrape =
|
||||||
|
(tracker.lastScrapeResult ? `${tracker.lastScrapeResult} - ` : '') +
|
||||||
|
lastScrapeTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: lastScrapeLabel,
|
||||||
|
value: lastScrape,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateTiers() {
|
||||||
|
const na = 'N/A';
|
||||||
|
const { list } = this.elements.tiers;
|
||||||
|
const { torrents } = this;
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (const tor of torrents) {
|
||||||
|
const group = document.createElement('div');
|
||||||
|
group.classList.add('inspector-group');
|
||||||
|
rows.push(group);
|
||||||
|
|
||||||
|
// if >1 torrent to be shown, give a title
|
||||||
|
if (torrents.length > 1) {
|
||||||
|
const title = document.createElement('div');
|
||||||
|
title.classList.add('tier-list-torrent');
|
||||||
|
setTextContent(title, tor.getName());
|
||||||
|
rows.push(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
tor.getTrackers().forEach((tracker, index) => {
|
||||||
|
const announceState = Inspector.getAnnounceState(tracker);
|
||||||
|
const lastAnnounceStatusHash = Inspector.lastAnnounceStatus(tracker);
|
||||||
|
const lastScrapeStatusHash = Inspector.lastScrapeStatus(tracker);
|
||||||
|
|
||||||
|
const tier_div = document.createElement('div');
|
||||||
|
tier_div.classList.add('tier-list-row', index % 2 ? 'odd' : 'even');
|
||||||
|
|
||||||
|
let element = document.createElement('div');
|
||||||
|
element.classList.add('tier-list-tracker');
|
||||||
|
setTextContent(
|
||||||
|
element,
|
||||||
|
`${tracker.domain || tracker.host || tracker.announce} - tier ${
|
||||||
|
tracker.tier + 1
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
element.setAttribute('title', tracker.announce);
|
||||||
|
tier_div.append(element);
|
||||||
|
|
||||||
|
element = document.createElement('div');
|
||||||
|
element.classList.add('tier-announce');
|
||||||
|
setTextContent(
|
||||||
|
element,
|
||||||
|
`${lastAnnounceStatusHash.label}: ${lastAnnounceStatusHash.value}`
|
||||||
|
);
|
||||||
|
tier_div.append(element);
|
||||||
|
|
||||||
|
element = document.createElement('div');
|
||||||
|
element.classList.add('tier-seeders');
|
||||||
|
setTextContent(
|
||||||
|
element,
|
||||||
|
`Seeders: ${tracker.seederCount > -1 ? tracker.seederCount : na}`
|
||||||
|
);
|
||||||
|
tier_div.append(element);
|
||||||
|
|
||||||
|
element = document.createElement('div');
|
||||||
|
element.classList.add('tier-state');
|
||||||
|
setTextContent(element, announceState);
|
||||||
|
tier_div.append(element);
|
||||||
|
|
||||||
|
element = document.createElement('div');
|
||||||
|
element.classList.add('tier-leechers');
|
||||||
|
setTextContent(
|
||||||
|
element,
|
||||||
|
`Leechers: ${tracker.leecherCount > -1 ? tracker.leecherCount : na}`
|
||||||
|
);
|
||||||
|
tier_div.append(element);
|
||||||
|
|
||||||
|
element = document.createElement('div');
|
||||||
|
element.classList.add('tier-scrape');
|
||||||
|
setTextContent(
|
||||||
|
element,
|
||||||
|
`${lastScrapeStatusHash.label}: ${lastScrapeStatusHash.value}`
|
||||||
|
);
|
||||||
|
tier_div.append(element);
|
||||||
|
|
||||||
|
element = document.createElement('div');
|
||||||
|
element.classList.add('tier-downloads');
|
||||||
|
setTextContent(
|
||||||
|
element,
|
||||||
|
`Downloads: ${
|
||||||
|
tracker.downloadCount > -1 ? tracker.downloadCount : na
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
tier_div.append(element);
|
||||||
|
|
||||||
|
rows.push(tier_div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: modify instead of rebuilding wholesale?
|
||||||
|
while (list.firstChild) {
|
||||||
|
list.firstChild.remove();
|
||||||
|
}
|
||||||
|
list.append(...rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FILES PAGE
|
||||||
|
|
||||||
|
_changeFileCommand(fileIndices, command) {
|
||||||
|
const { controller, file_torrent } = this;
|
||||||
|
const torrentId = file_torrent.getId();
|
||||||
|
controller.changeFileCommand(torrentId, fileIndices, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onFileWantedToggled(event_) {
|
||||||
|
const { indices, wanted } = event_;
|
||||||
|
this._changeFileCommand(
|
||||||
|
indices,
|
||||||
|
wanted ? 'files-wanted' : 'files-unwanted'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onFilePriorityToggled(event_) {
|
||||||
|
const { indices, priority } = event_;
|
||||||
|
|
||||||
|
let command = null;
|
||||||
|
switch (priority) {
|
||||||
|
case -1:
|
||||||
|
command = 'priority-low';
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
command = 'priority-high';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
command = 'priority-normal';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._changeFileCommand(indices, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearFileList() {
|
||||||
|
const { list } = this.elements.files;
|
||||||
|
while (list.firstChild) {
|
||||||
|
list.firstChild.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.file_torrent = null;
|
||||||
|
this.file_torrent_n = null;
|
||||||
|
this.file_rows = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static createFileTreeModel(tor) {
|
||||||
|
const leaves = [];
|
||||||
|
const tree = {
|
||||||
|
children: {},
|
||||||
|
file_indices: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
tor.getFiles().forEach((file, index) => {
|
||||||
|
const { name } = file;
|
||||||
|
const tokens = name.split('/');
|
||||||
|
let walk = tree;
|
||||||
|
for (const [index_, token] of tokens.entries()) {
|
||||||
|
let sub = walk.children[token];
|
||||||
|
if (!sub) {
|
||||||
|
walk.children[token] = sub = {
|
||||||
|
children: {},
|
||||||
|
depth: index_,
|
||||||
|
file_indices: [],
|
||||||
|
name: token,
|
||||||
|
parent: walk,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
walk = sub;
|
||||||
|
}
|
||||||
|
walk.file_index = index;
|
||||||
|
delete walk.children;
|
||||||
|
leaves.push(walk);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const leaf of leaves) {
|
||||||
|
const { file_index } = leaf;
|
||||||
|
let walk = leaf;
|
||||||
|
do {
|
||||||
|
walk.file_indices.push(file_index);
|
||||||
|
walk = walk.parent;
|
||||||
|
} while (walk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
addNodeToView(tor, parent, sub, index) {
|
||||||
|
const row = new FileRow(
|
||||||
|
tor,
|
||||||
|
sub.depth,
|
||||||
|
sub.name,
|
||||||
|
sub.file_indices,
|
||||||
|
index % 2
|
||||||
|
);
|
||||||
|
row.addEventListener('wantedToggled', this._onFileWantedToggled.bind(this));
|
||||||
|
row.addEventListener(
|
||||||
|
'priorityToggled',
|
||||||
|
this._onFilePriorityToggled.bind(this)
|
||||||
|
);
|
||||||
|
this.file_rows.push(row);
|
||||||
|
parent.append(row.getElement());
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubtreeToView(tor, parent, sub, index) {
|
||||||
|
if (sub.parent) {
|
||||||
|
this.addNodeToView(tor, parent, sub, index++);
|
||||||
|
}
|
||||||
|
if (sub.children) {
|
||||||
|
for (const value of Object.values(sub.children)) {
|
||||||
|
index = this.addSubtreeToView(tor, parent, value, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateFiles() {
|
||||||
|
const { list } = this.elements.files;
|
||||||
|
const { torrents } = this;
|
||||||
|
|
||||||
|
// only show one torrent at a time
|
||||||
|
if (torrents.length !== 1) {
|
||||||
|
this._clearFileList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [tor] = torrents;
|
||||||
|
const n = tor.getFiles().length;
|
||||||
|
if (tor !== this.file_torrent || n !== this.file_torrent_n) {
|
||||||
|
// rebuild the file list...
|
||||||
|
this._clearFileList();
|
||||||
|
this.file_torrent = tor;
|
||||||
|
this.file_torrent_n = n;
|
||||||
|
this.file_rows = [];
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
const tree = Inspector.createFileTreeModel(tor);
|
||||||
|
this.addSubtreeToView(tor, fragment, tree, 0);
|
||||||
|
list.append(fragment);
|
||||||
|
} else {
|
||||||
|
// ...refresh the already-existing file list
|
||||||
|
this.file_rows.forEach((row) => row.refresh());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ActionManager } from './action-manager.js';
|
||||||
|
import { Notifications } from './notifications.js';
|
||||||
|
import { Prefs } from './prefs.js';
|
||||||
|
import { Transmission } from './transmission.js';
|
||||||
|
import { debounce } from './utils.js';
|
||||||
|
|
||||||
|
import '../style/transmission-app.scss';
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const action_manager = new ActionManager();
|
||||||
|
const prefs = new Prefs();
|
||||||
|
const notifications = new Notifications(prefs);
|
||||||
|
const transmission = new Transmission(action_manager, notifications, prefs);
|
||||||
|
|
||||||
|
const scroll_soon = debounce(() =>
|
||||||
|
transmission.elements.torrent_list.scrollTo(0, 1)
|
||||||
|
);
|
||||||
|
window.addEventListener('load', scroll_soon);
|
||||||
|
window.onorientationchange = scroll_soon;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', main);
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let default_path = '';
|
||||||
|
|
||||||
|
import { createDialogContainer } from './utils.js';
|
||||||
|
|
||||||
|
export class MoveDialog extends EventTarget {
|
||||||
|
constructor(controller, remote) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.controller = controller;
|
||||||
|
this.remote = remote;
|
||||||
|
this.elements = {};
|
||||||
|
this.torrents = [];
|
||||||
|
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
const torrents = this.controller.getSelectedTorrents();
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
default_path = default_path || torrents[0].getDownloadDir();
|
||||||
|
|
||||||
|
this.torrents = torrents;
|
||||||
|
this.elements = MoveDialog._create();
|
||||||
|
this.elements.confirm.addEventListener('click', () => this._onConfirm());
|
||||||
|
this.elements.dismiss.addEventListener('click', () => this._onDismiss());
|
||||||
|
this.elements.entry.value = default_path;
|
||||||
|
document.body.append(this.elements.root);
|
||||||
|
|
||||||
|
this.elements.entry.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.elements.root.remove();
|
||||||
|
|
||||||
|
this.dispatchEvent(new Event('close'));
|
||||||
|
|
||||||
|
delete this.controller;
|
||||||
|
delete this.remote;
|
||||||
|
delete this.elements;
|
||||||
|
delete this.torrents;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDismiss() {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onConfirm() {
|
||||||
|
const ids = this.torrents.map((tor) => tor.getId());
|
||||||
|
const path = this.elements.entry.value.trim();
|
||||||
|
default_path = path;
|
||||||
|
this.remote.moveTorrents(ids, path);
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static _create() {
|
||||||
|
const elements = createDialogContainer('move-dialog');
|
||||||
|
elements.root.setAttribute('aria-label', 'Move Torrent');
|
||||||
|
elements.heading.textContent = 'Set Torrent Location';
|
||||||
|
confirm.textContent = 'Apply';
|
||||||
|
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.setAttribute('for', 'torrent-path');
|
||||||
|
label.textContent = 'Location:';
|
||||||
|
elements.workarea.append(label);
|
||||||
|
|
||||||
|
const entry = document.createElement('input');
|
||||||
|
entry.setAttribute('type', 'text');
|
||||||
|
entry.id = 'torrent-path';
|
||||||
|
elements.entry = entry;
|
||||||
|
elements.workarea.append(entry);
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { setTextContent } from './utils.js';
|
||||||
|
|
||||||
|
export class Notifications {
|
||||||
|
constructor(prefs) {
|
||||||
|
this._prefs = prefs;
|
||||||
|
this._elements = {
|
||||||
|
toggle: document.querySelector('#toggle-notifications'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_setEnabled(enabled) {
|
||||||
|
this.prefs.notifications_enabled = enabled;
|
||||||
|
setTextContent(
|
||||||
|
this._toggle,
|
||||||
|
`${enabled ? 'Disable' : 'Enable'} Notifications`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestPermission() {
|
||||||
|
Notification.requestPermission().then((s) =>
|
||||||
|
this._setEnabled(s === 'granted')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (this._enabled) {
|
||||||
|
this._setEnabled(false);
|
||||||
|
} else if (Notification.permission === 'granted') {
|
||||||
|
this._setEnabled(true);
|
||||||
|
} else if (Notification.permission !== 'denied') {
|
||||||
|
this._requestPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// TODO:
|
||||||
|
// $(transmission).bind('downloadComplete seedingComplete', (event, torrent) => {
|
||||||
|
// if (notificationsEnabled) {
|
||||||
|
const title = `${event.type === 'downloadComplete' ? 'Download' : 'Seeding'} complete`;
|
||||||
|
const content = torrent.getName();
|
||||||
|
const notification = window.webkitNotifications.createNotification(
|
||||||
|
'style/transmission/images/logo.png',
|
||||||
|
title,
|
||||||
|
content
|
||||||
|
);
|
||||||
|
notification.show();
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.cancel();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}
|
|
@ -0,0 +1,194 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AlertDialog } from './alert-dialog.js';
|
||||||
|
import { Formatter } from './formatter.js';
|
||||||
|
import { createDialogContainer, makeUUID } from './utils.js';
|
||||||
|
|
||||||
|
export class OpenDialog extends EventTarget {
|
||||||
|
constructor(controller, remote) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.controller = controller;
|
||||||
|
this.remote = remote;
|
||||||
|
|
||||||
|
this.elements = this._create();
|
||||||
|
this.elements.dismiss.addEventListener('click', () => this._onDismiss());
|
||||||
|
this.elements.confirm.addEventListener('click', () => this._onConfirm());
|
||||||
|
this._updateFreeSpaceInAddDialog();
|
||||||
|
document.body.append(this.elements.root);
|
||||||
|
this.elements.url_input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.closed) {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
|
||||||
|
this.elements.root.remove();
|
||||||
|
this.dispatchEvent(new Event('close'));
|
||||||
|
|
||||||
|
for (const key of Object.keys(this)) {
|
||||||
|
delete this[key];
|
||||||
|
}
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDismiss() {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateFreeSpaceInAddDialog() {
|
||||||
|
const path = this.elements.folder_input.value;
|
||||||
|
this.remote.getFreeSpace(path, (dir, bytes) => {
|
||||||
|
const string = bytes > 0 ? `${Formatter.size(bytes)} Free` : '';
|
||||||
|
this.elements.freespace.textContent = string;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onConfirm() {
|
||||||
|
const { remote } = this;
|
||||||
|
const { file_input, folder_input, start_input, url_input } = this.elements;
|
||||||
|
const paused = !start_input.checked;
|
||||||
|
const destination = folder_input.value.trim();
|
||||||
|
|
||||||
|
for (const file of file_input.files) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('load', (e) => {
|
||||||
|
const contents = e.target.result;
|
||||||
|
const key = 'base64,';
|
||||||
|
const index = contents.indexOf(key);
|
||||||
|
if (index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const o = {
|
||||||
|
arguments: {
|
||||||
|
'download-dir': destination,
|
||||||
|
metainfo: contents.slice(Math.max(0, index + key.length)),
|
||||||
|
paused,
|
||||||
|
},
|
||||||
|
method: 'torrent-add',
|
||||||
|
};
|
||||||
|
console.log(o);
|
||||||
|
remote.sendRequest(o, (response) => {
|
||||||
|
if (response.result !== 'success') {
|
||||||
|
alert(`Error adding "${file.name}": ${response.result}`);
|
||||||
|
this.controller.setCurrentPopup(
|
||||||
|
new AlertDialog({
|
||||||
|
heading: `Error adding "${file.name}"`,
|
||||||
|
message: response.result,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = url_input.value.trim();
|
||||||
|
if (url.length > 0) {
|
||||||
|
if (url.match(/^[\da-f]{40}$/i)) {
|
||||||
|
url = `magnet:?xt=urn:btih:${url}`;
|
||||||
|
}
|
||||||
|
const o = {
|
||||||
|
arguments: {
|
||||||
|
'download-dir': destination,
|
||||||
|
filename: url,
|
||||||
|
paused,
|
||||||
|
},
|
||||||
|
method: 'torrent-add',
|
||||||
|
};
|
||||||
|
console.log(o);
|
||||||
|
remote.sendRequest(o, (payload, response) => {
|
||||||
|
if (response.result !== 'success') {
|
||||||
|
this.controller.setCurrentPopup(
|
||||||
|
new AlertDialog({
|
||||||
|
heading: `Error adding "${url}"`,
|
||||||
|
message: response.result,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this._onDismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
_create() {
|
||||||
|
const elements = createDialogContainer();
|
||||||
|
const { confirm, root, heading, workarea } = elements;
|
||||||
|
|
||||||
|
root.classList.add('open-torrent');
|
||||||
|
heading.textContent = 'Add Torrents';
|
||||||
|
confirm.textContent = 'Add';
|
||||||
|
|
||||||
|
let input_id = makeUUID();
|
||||||
|
let label = document.createElement('label');
|
||||||
|
label.setAttribute('for', input_id);
|
||||||
|
label.textContent = 'Please select torrent files to add:';
|
||||||
|
workarea.append(label);
|
||||||
|
|
||||||
|
let input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.name = 'torrent-files[]';
|
||||||
|
input.id = input_id;
|
||||||
|
input.multiple = 'multiple';
|
||||||
|
workarea.append(input);
|
||||||
|
elements.file_input = input;
|
||||||
|
|
||||||
|
input_id = makeUUID();
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.setAttribute('for', input_id);
|
||||||
|
label.textContent = 'Or enter a URL:';
|
||||||
|
workarea.append(label);
|
||||||
|
|
||||||
|
input = document.createElement('input');
|
||||||
|
input.type = 'url';
|
||||||
|
input.id = input_id;
|
||||||
|
workarea.append(input);
|
||||||
|
elements.url_input = input;
|
||||||
|
|
||||||
|
input_id = makeUUID();
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.id = 'add-dialog-folder-label';
|
||||||
|
label.for = input_id;
|
||||||
|
label.textContent = 'Destination folder:';
|
||||||
|
workarea.append(label);
|
||||||
|
|
||||||
|
const freespace = document.createElement('span');
|
||||||
|
freespace.id = 'free-space-text';
|
||||||
|
label.append(freespace);
|
||||||
|
workarea.append(label);
|
||||||
|
elements.freespace = freespace;
|
||||||
|
|
||||||
|
input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.id = 'add-dialog-folder-input';
|
||||||
|
input.addEventListener('change', () => this._updateFreeSpaceInAddDialog());
|
||||||
|
input.value = this.controller.session_properties['download-dir'];
|
||||||
|
workarea.append(input);
|
||||||
|
elements.folder_input = input;
|
||||||
|
|
||||||
|
const checkarea = document.createElement('div');
|
||||||
|
workarea.append(checkarea);
|
||||||
|
|
||||||
|
const check = document.createElement('input');
|
||||||
|
check.type = 'checkbox';
|
||||||
|
check.id = 'auto-start-check';
|
||||||
|
check.checked = this.controller.shouldAddedTorrentsStart();
|
||||||
|
checkarea.append(check);
|
||||||
|
elements.start_input = check;
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.id = 'auto-start-label';
|
||||||
|
label.setAttribute('for', check.id);
|
||||||
|
label.textContent = 'Start when added';
|
||||||
|
checkarea.append(label);
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,466 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Formatter } from './formatter.js';
|
||||||
|
import { Prefs } from './prefs.js';
|
||||||
|
import { RPC } from './remote.js';
|
||||||
|
import { OutsideClickListener, setEnabled } from './utils.js';
|
||||||
|
|
||||||
|
export class OverflowMenu extends EventTarget {
|
||||||
|
constructor(session_manager, prefs, remote, action_manager) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.action_listener = this._onActionChange.bind(this);
|
||||||
|
this.action_manager = action_manager;
|
||||||
|
this.action_manager.addEventListener('change', this.action_listener);
|
||||||
|
|
||||||
|
this.prefs_listener = this._onPrefsChange.bind(this);
|
||||||
|
this.prefs = prefs;
|
||||||
|
this.prefs.addEventListener('change', this.prefs_listener);
|
||||||
|
|
||||||
|
this.closed = false;
|
||||||
|
this.remote = remote;
|
||||||
|
this.name = 'overflow-menu';
|
||||||
|
|
||||||
|
this.session_listener = this._onSessionChange.bind(this);
|
||||||
|
this.session_manager = session_manager;
|
||||||
|
this.session_manager.addEventListener(
|
||||||
|
'session-change',
|
||||||
|
this.session_listener
|
||||||
|
);
|
||||||
|
|
||||||
|
const { session_properties } = session_manager;
|
||||||
|
Object.assign(this, this._create(session_properties));
|
||||||
|
|
||||||
|
this.outside = new OutsideClickListener(this.root);
|
||||||
|
this.outside.addEventListener('click', () => this.close());
|
||||||
|
Object.seal(this);
|
||||||
|
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
document.body.append(this.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.closed) {
|
||||||
|
this.outside.stop();
|
||||||
|
this.session_manager.removeEventListener(
|
||||||
|
'session-change',
|
||||||
|
this.session_listener
|
||||||
|
);
|
||||||
|
this.action_manager.removeEventListener('change', this.action_listener);
|
||||||
|
this.prefs.removeEventListener('change', this.prefs_listener);
|
||||||
|
|
||||||
|
this.root.remove();
|
||||||
|
this.dispatchEvent(new Event('close'));
|
||||||
|
|
||||||
|
for (const key of Object.keys(this)) {
|
||||||
|
this[key] = null;
|
||||||
|
}
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSessionChange(event_) {
|
||||||
|
const { alt_speed_check } = this.elements;
|
||||||
|
const { session_properties } = event_;
|
||||||
|
alt_speed_check.checked = session_properties[RPC._TurtleState];
|
||||||
|
}
|
||||||
|
|
||||||
|
_onPrefsChange(event_) {
|
||||||
|
switch (event_.key) {
|
||||||
|
case Prefs.SortDirection:
|
||||||
|
case Prefs.SortMode:
|
||||||
|
this.root.querySelector(`[data-pref="${event_.key}"]`).value =
|
||||||
|
event_.value;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onActionChange(event_) {
|
||||||
|
const element = this.actions[event_.action];
|
||||||
|
if (element) {
|
||||||
|
this._updateElement(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateElement(element) {
|
||||||
|
if (element.dataset.action) {
|
||||||
|
const { action } = element.dataset;
|
||||||
|
const shortcuts = this.action_manager.keyshortcuts(action);
|
||||||
|
if (shortcuts) {
|
||||||
|
element.setAttribute('aria-keyshortcuts', shortcuts);
|
||||||
|
}
|
||||||
|
setEnabled(element, this.action_manager.isEnabled(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClick(event_) {
|
||||||
|
const { action, pref } = event_.target.dataset;
|
||||||
|
|
||||||
|
if (action) {
|
||||||
|
this.action_manager.click(action);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pref) {
|
||||||
|
this.prefs[pref] = event_.target.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('unhandled');
|
||||||
|
console.log(event_);
|
||||||
|
console.trace();
|
||||||
|
}
|
||||||
|
|
||||||
|
_create(session_properties) {
|
||||||
|
const actions = {};
|
||||||
|
const on_click = this._onClick.bind(this);
|
||||||
|
const elements = {};
|
||||||
|
|
||||||
|
const make_section = (classname, title) => {
|
||||||
|
const section = document.createElement('fieldset');
|
||||||
|
section.classList.add('section', classname);
|
||||||
|
const legend = document.createElement('legend');
|
||||||
|
legend.classList.add('title');
|
||||||
|
legend.textContent = title;
|
||||||
|
section.append(legend);
|
||||||
|
return section;
|
||||||
|
};
|
||||||
|
|
||||||
|
const make_button = (parent, text, action) => {
|
||||||
|
const e = document.createElement('button');
|
||||||
|
e.textContent = text;
|
||||||
|
e.addEventListener('click', on_click);
|
||||||
|
parent.append(e);
|
||||||
|
if (action) {
|
||||||
|
e.dataset.action = action;
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
const root = document.createElement('div');
|
||||||
|
root.classList.add('overflow-menu', 'popup');
|
||||||
|
|
||||||
|
let section = make_section('display', 'Display');
|
||||||
|
root.append(section);
|
||||||
|
|
||||||
|
let options = document.createElement('div');
|
||||||
|
options.id = 'display-options';
|
||||||
|
section.append(options);
|
||||||
|
|
||||||
|
// sort mode
|
||||||
|
|
||||||
|
let div = document.createElement('div');
|
||||||
|
options.append(div);
|
||||||
|
|
||||||
|
let label = document.createElement('label');
|
||||||
|
label.id = 'display-sort-mode-label';
|
||||||
|
label.textContent = 'Sort by';
|
||||||
|
div.append(label);
|
||||||
|
|
||||||
|
let select = document.createElement('select');
|
||||||
|
select.id = 'display-sort-mode-select';
|
||||||
|
select.dataset.pref = Prefs.SortMode;
|
||||||
|
div.append(select);
|
||||||
|
|
||||||
|
const sort_modes = [
|
||||||
|
[Prefs.SortByActivity, 'Activity'],
|
||||||
|
[Prefs.SortByAge, 'Age'],
|
||||||
|
[Prefs.SortByName, 'Name'],
|
||||||
|
[Prefs.SortByProgress, 'Progress'],
|
||||||
|
[Prefs.SortByQueue, 'Queue order'],
|
||||||
|
[Prefs.SortByRatio, 'Ratio'],
|
||||||
|
[Prefs.SortBySize, 'Size'],
|
||||||
|
[Prefs.SortByState, 'State'],
|
||||||
|
];
|
||||||
|
for (const [value, text] of sort_modes) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = value;
|
||||||
|
option.textContent = text;
|
||||||
|
select.append(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
label.setAttribute('for', select.id);
|
||||||
|
select.value = this.prefs.sort_mode;
|
||||||
|
select.addEventListener('change', (event_) => {
|
||||||
|
this.prefs.sort_mode = event_.target.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// sort direction
|
||||||
|
|
||||||
|
div = document.createElement('div');
|
||||||
|
options.append(div);
|
||||||
|
|
||||||
|
let check = document.createElement('input');
|
||||||
|
check.id = 'display-sort-reverse-check';
|
||||||
|
check.dataset.pref = Prefs.SortDirection;
|
||||||
|
check.type = 'checkbox';
|
||||||
|
div.append(check);
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.id = 'display-sort-reverse-label';
|
||||||
|
label.setAttribute('for', check.id);
|
||||||
|
label.textContent = 'Reverse sort';
|
||||||
|
div.append(label);
|
||||||
|
|
||||||
|
check.checked = this.prefs.sort_direction !== Prefs.SortAscending;
|
||||||
|
check.addEventListener('input', (event_) => {
|
||||||
|
this.prefs.sort_direction = event_.target.checked
|
||||||
|
? Prefs.SortDescending
|
||||||
|
: Prefs.SortAscending;
|
||||||
|
});
|
||||||
|
|
||||||
|
// compact
|
||||||
|
|
||||||
|
div = document.createElement('div');
|
||||||
|
options.append(div);
|
||||||
|
|
||||||
|
const action = 'toggle-compact-rows';
|
||||||
|
check = document.createElement('input');
|
||||||
|
check.id = 'display-compact-check';
|
||||||
|
check.dataset.action = action;
|
||||||
|
check.type = 'checkbox';
|
||||||
|
div.append(check);
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.id = 'display-compact-label';
|
||||||
|
label.for = check.id;
|
||||||
|
label.setAttribute('for', check.id);
|
||||||
|
label.textContent = this.action_manager.text(action);
|
||||||
|
div.append(label);
|
||||||
|
|
||||||
|
check.checked = this.prefs.display_mode === Prefs.DisplayCompact;
|
||||||
|
check.addEventListener('input', (event_) => {
|
||||||
|
const { checked } = event_.target;
|
||||||
|
this.prefs.display_mode = checked
|
||||||
|
? Prefs.DisplayCompact
|
||||||
|
: Prefs.DisplayFull;
|
||||||
|
});
|
||||||
|
|
||||||
|
// fullscreen
|
||||||
|
|
||||||
|
div = document.createElement('div');
|
||||||
|
options.append(div);
|
||||||
|
|
||||||
|
check = document.createElement('input');
|
||||||
|
check.id = 'display-fullscreen-check';
|
||||||
|
check.type = 'checkbox';
|
||||||
|
const is_fullscreen = () => document.fullscreenElement !== null;
|
||||||
|
check.checked = is_fullscreen();
|
||||||
|
check.addEventListener('input', () => {
|
||||||
|
if (is_fullscreen()) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else {
|
||||||
|
document.body.requestFullscreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener('fullscreenchange', () => {
|
||||||
|
check.checked = is_fullscreen();
|
||||||
|
});
|
||||||
|
div.append(check);
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.id = 'display-fullscreen-label';
|
||||||
|
label.for = check.id;
|
||||||
|
label.setAttribute('for', check.id);
|
||||||
|
label.textContent = 'Fullscreen';
|
||||||
|
div.append(label);
|
||||||
|
|
||||||
|
section = make_section('speed', 'Speed Limit');
|
||||||
|
root.append(section);
|
||||||
|
|
||||||
|
options = document.createElement('div');
|
||||||
|
options.id = 'speed-options';
|
||||||
|
section.append(options);
|
||||||
|
|
||||||
|
// speed up
|
||||||
|
|
||||||
|
div = document.createElement('div');
|
||||||
|
div.classList.add('speed-up');
|
||||||
|
options.append(div);
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.id = 'speed-up-label';
|
||||||
|
label.textContent = 'Upload:';
|
||||||
|
div.append(label);
|
||||||
|
|
||||||
|
const unlimited = 'Unlimited';
|
||||||
|
select = document.createElement('select');
|
||||||
|
select.id = 'speed-up-select';
|
||||||
|
div.append(select);
|
||||||
|
|
||||||
|
const speeds = ['10', '100', '200', '500', '750', unlimited];
|
||||||
|
for (const speed of [
|
||||||
|
...new Set(speeds)
|
||||||
|
.add(`${session_properties[RPC._UpSpeedLimit]}`)
|
||||||
|
.values(),
|
||||||
|
].sort()) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = speed;
|
||||||
|
option.textContent =
|
||||||
|
speed === unlimited ? unlimited : Formatter.speed(speed);
|
||||||
|
select.append(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
label.setAttribute('for', select.id);
|
||||||
|
select.value = session_properties[RPC._UpSpeedLimited]
|
||||||
|
? `${session_properties[RPC._UpSpeedLimit]}`
|
||||||
|
: unlimited;
|
||||||
|
select.addEventListener('change', (event_) => {
|
||||||
|
const { value } = event_.target;
|
||||||
|
console.log(event_);
|
||||||
|
if (event_.target.value === unlimited) {
|
||||||
|
this.remote.savePrefs({ [RPC._UpSpeedLimited]: false });
|
||||||
|
} else {
|
||||||
|
this.remote.savePrefs({
|
||||||
|
[RPC._UpSpeedLimited]: true,
|
||||||
|
[RPC._UpSpeedLimit]: Number.parseInt(value, 10),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// speed down
|
||||||
|
|
||||||
|
div = document.createElement('div');
|
||||||
|
div.classList.add('speed-down');
|
||||||
|
options.append(div);
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.id = 'speed-down-label';
|
||||||
|
label.textContent = 'Download:';
|
||||||
|
div.append(label);
|
||||||
|
|
||||||
|
select = document.createElement('select');
|
||||||
|
select.id = 'speed-down-select';
|
||||||
|
div.append(select);
|
||||||
|
|
||||||
|
for (const speed of [
|
||||||
|
...new Set(speeds)
|
||||||
|
.add(`${session_properties[RPC._DownSpeedLimit]}`)
|
||||||
|
.values(),
|
||||||
|
].sort()) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = speed;
|
||||||
|
option.textContent = speed;
|
||||||
|
select.append(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
label.setAttribute('for', select.id);
|
||||||
|
select.value = session_properties[RPC._DownSpeedLimited]
|
||||||
|
? `${session_properties[RPC._DownSpeedLimit]}`
|
||||||
|
: unlimited;
|
||||||
|
select.addEventListener('change', (event_) => {
|
||||||
|
const { value } = event_.target;
|
||||||
|
console.log(event_);
|
||||||
|
if (event_.target.value === unlimited) {
|
||||||
|
this.remote.savePrefs({ [RPC._DownSpeedLimited]: false });
|
||||||
|
} else {
|
||||||
|
this.remote.savePrefs({
|
||||||
|
[RPC._DownSpeedLimited]: true,
|
||||||
|
[RPC._DownSpeedLimit]: Number.parseInt(value, 10),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// alt speed
|
||||||
|
|
||||||
|
div = document.createElement('div');
|
||||||
|
div.classList.add('alt-speed');
|
||||||
|
options.append(div);
|
||||||
|
|
||||||
|
check = document.createElement('input');
|
||||||
|
check.id = 'alt-speed-check';
|
||||||
|
check.type = 'checkbox';
|
||||||
|
check.checked = session_properties[RPC._TurtleState];
|
||||||
|
check.addEventListener('change', (event_) => {
|
||||||
|
this.remote.savePrefs({
|
||||||
|
[RPC._TurtleState]: event_.target.checked,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
div.append(check);
|
||||||
|
elements.alt_speed_check = check;
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.id = 'alt-speed-image';
|
||||||
|
label.setAttribute('for', check.id);
|
||||||
|
div.append(label);
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.id = 'alt-speed-label';
|
||||||
|
label.setAttribute('for', check.id);
|
||||||
|
label.textContent = 'Use Temp limits';
|
||||||
|
div.append(label);
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.id = 'alt-speed-values-label';
|
||||||
|
label.setAttribute('for', check.id);
|
||||||
|
|
||||||
|
const up = Formatter.speed(session_properties[RPC._TurtleUpSpeedLimit]);
|
||||||
|
const dn = Formatter.speed(session_properties[RPC._TurtleDownSpeedLimit]);
|
||||||
|
label.textContent = `(${up} up, ${dn} down)`;
|
||||||
|
div.append(label);
|
||||||
|
|
||||||
|
section = make_section('actions', 'Actions');
|
||||||
|
root.append(section);
|
||||||
|
|
||||||
|
for (const action_name of [
|
||||||
|
'show-preferences-dialog',
|
||||||
|
'pause-all-torrents',
|
||||||
|
'start-all-torrents',
|
||||||
|
]) {
|
||||||
|
const text = this.action_manager.text(action_name);
|
||||||
|
actions[action_name] = make_button(section, text, action_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
section = make_section('info', 'Info');
|
||||||
|
root.append(section);
|
||||||
|
|
||||||
|
options = document.createElement('div');
|
||||||
|
section.append(options);
|
||||||
|
|
||||||
|
for (const action_name of [
|
||||||
|
'show-about-dialog',
|
||||||
|
'show-shortcuts-dialog',
|
||||||
|
'show-statistics-dialog',
|
||||||
|
]) {
|
||||||
|
const text = this.action_manager.text(action_name);
|
||||||
|
actions[action_name] = make_button(options, text, action_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
section = make_section('links', 'Links');
|
||||||
|
root.append(section);
|
||||||
|
|
||||||
|
options = document.createElement('div');
|
||||||
|
section.append(options);
|
||||||
|
|
||||||
|
let e = document.createElement('a');
|
||||||
|
e.href = 'https://transmissionbt.com/';
|
||||||
|
e.tabindex = '0';
|
||||||
|
e.textContent = 'Homepage';
|
||||||
|
options.append(e);
|
||||||
|
|
||||||
|
e = document.createElement('a');
|
||||||
|
e.href = 'https://transmissionbt.com/donate/';
|
||||||
|
e.tabindex = '0';
|
||||||
|
e.textContent = 'Tip Jar';
|
||||||
|
options.append(e);
|
||||||
|
|
||||||
|
e = document.createElement('a');
|
||||||
|
e.href = 'https://github.com/transmission/transmission/';
|
||||||
|
e.tabindex = '0';
|
||||||
|
e.textContent = 'Source Code';
|
||||||
|
options.append(e);
|
||||||
|
|
||||||
|
Object.values(actions).forEach(this._updateElement.bind(this));
|
||||||
|
return { actions, elements, root };
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,710 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Formatter } from './formatter.js';
|
||||||
|
import {
|
||||||
|
OutsideClickListener,
|
||||||
|
createTabsContainer,
|
||||||
|
makeUUID,
|
||||||
|
setEnabled,
|
||||||
|
setTextContent,
|
||||||
|
} from './utils.js';
|
||||||
|
|
||||||
|
export class PrefsDialog extends EventTarget {
|
||||||
|
static _initTimeDropDown(e) {
|
||||||
|
for (let index = 0; index < 24 * 4; ++index) {
|
||||||
|
const hour = Number.parseInt(index / 4, 10);
|
||||||
|
const mins = (index % 4) * 15;
|
||||||
|
const value = index * 15;
|
||||||
|
const content = `${hour}:${mins || '00'}`;
|
||||||
|
e.options[index] = new Option(content, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static _initDayDropDown(e) {
|
||||||
|
const options = [
|
||||||
|
['Everyday', '127'],
|
||||||
|
['Weekdays', '62'],
|
||||||
|
['Weekends', '65'],
|
||||||
|
['Sunday', '1'],
|
||||||
|
['Monday', '2'],
|
||||||
|
['Tuesday', '4'],
|
||||||
|
['Wednesday', '8'],
|
||||||
|
['Thursday', '16'],
|
||||||
|
['Friday', '32'],
|
||||||
|
['Saturday', '64'],
|
||||||
|
];
|
||||||
|
for (let index = 0; options[index]; ++index) {
|
||||||
|
const [text, value] = options[index];
|
||||||
|
e.options[index] = new Option(text, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_checkPort() {
|
||||||
|
const element = this.elements.network.port_status_label;
|
||||||
|
element.removeAttribute('data-open');
|
||||||
|
setTextContent(element, 'Checking...');
|
||||||
|
this.remote.checkPort(this._onPortChecked, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onPortChecked(response) {
|
||||||
|
const element = this.elements.network.port_status_label;
|
||||||
|
const is_open = response.arguments['port-is-open'];
|
||||||
|
element.dataset.open = is_open;
|
||||||
|
setTextContent(element, is_open ? 'Open' : 'Closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
_setBlocklistButtonEnabled(b) {
|
||||||
|
const e = this.elements.peers.blocklist_update_button;
|
||||||
|
setEnabled(e, b);
|
||||||
|
e.value = b ? 'Update' : 'Updating...';
|
||||||
|
}
|
||||||
|
|
||||||
|
static _getValue(e) {
|
||||||
|
switch (e.type) {
|
||||||
|
case 'checkbox':
|
||||||
|
case 'radio':
|
||||||
|
return e.checked;
|
||||||
|
|
||||||
|
case 'number':
|
||||||
|
case 'text':
|
||||||
|
case 'url': {
|
||||||
|
const string = e.value;
|
||||||
|
if (Number.parseInt(string, 10).toString() === string) {
|
||||||
|
return Number.parseInt(string, 10);
|
||||||
|
}
|
||||||
|
if (Number.parseFloat(string).toString() === string) {
|
||||||
|
return Number.parseFloat(string);
|
||||||
|
}
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this callback is for controls whose changes can be applied
|
||||||
|
// immediately, like checkboxs, radioboxes, and selects
|
||||||
|
_onControlChanged(event_) {
|
||||||
|
const { key } = event_.target.dataset;
|
||||||
|
this.remote.savePrefs({
|
||||||
|
[key]: PrefsDialog._getValue(event_.target),
|
||||||
|
});
|
||||||
|
if (key === 'peer-port' || key === 'port-forwarding-enabled') {
|
||||||
|
this._checkPort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDialogClosed() {
|
||||||
|
this.dispatchEvent(new Event('closed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the dialog's controls
|
||||||
|
_update(o) {
|
||||||
|
if (!o) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._setBlocklistButtonEnabled(true);
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(o)) {
|
||||||
|
for (const element of this.elements.root.querySelectorAll(
|
||||||
|
`[data-key="${key}"]`
|
||||||
|
)) {
|
||||||
|
if (key === 'blocklist-size') {
|
||||||
|
const n = Formatter.number(value);
|
||||||
|
element.innerHTML = `Blocklist has <span class="blocklist-size-number">${n}</span> rules`;
|
||||||
|
setTextContent(this.elements.peers.blocklist_update_button, 'Update');
|
||||||
|
} else {
|
||||||
|
switch (element.type) {
|
||||||
|
case 'checkbox':
|
||||||
|
case 'radio':
|
||||||
|
if (element.checked !== value) {
|
||||||
|
element.checked = value;
|
||||||
|
element.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'text':
|
||||||
|
case 'url':
|
||||||
|
case 'email':
|
||||||
|
case 'number':
|
||||||
|
case 'search':
|
||||||
|
// don't change the text if the user's editing it.
|
||||||
|
// it's very annoying when that happens!
|
||||||
|
if (
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
element.value != value &&
|
||||||
|
element !== document.activeElement
|
||||||
|
) {
|
||||||
|
element.value = value;
|
||||||
|
element.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'select-one':
|
||||||
|
if (element.value !== value) {
|
||||||
|
element.value = value;
|
||||||
|
element.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldAddedTorrentsStart() {
|
||||||
|
return this.data.elements.root.find('#start-added-torrents')[0].checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
static _createCheckAndLabel(id, text) {
|
||||||
|
const root = document.createElement('div');
|
||||||
|
root.id = id;
|
||||||
|
|
||||||
|
const check = document.createElement('input');
|
||||||
|
check.id = makeUUID();
|
||||||
|
check.type = 'checkbox';
|
||||||
|
root.append(check);
|
||||||
|
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.textContent = text;
|
||||||
|
label.setAttribute('for', check.id);
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
return { check, label, root };
|
||||||
|
}
|
||||||
|
|
||||||
|
static _enableIfChecked(element, check) {
|
||||||
|
const callback = () => {
|
||||||
|
if (element.tagName === 'INPUT') {
|
||||||
|
setEnabled(element, check.checked);
|
||||||
|
} else {
|
||||||
|
element.classList.toggle('disabled', !check.checked);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
check.addEventListener('change', callback);
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
static _createTorrentsPage() {
|
||||||
|
const root = document.createElement('div');
|
||||||
|
root.classList.add('prefs-torrents-page');
|
||||||
|
|
||||||
|
let label = document.createElement('div');
|
||||||
|
label.textContent = 'Downloading';
|
||||||
|
label.classList.add('section-label');
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.textContent = 'Download to:';
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
let input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.id = makeUUID();
|
||||||
|
input.dataset.key = 'download-dir';
|
||||||
|
label.setAttribute('for', input.id);
|
||||||
|
root.append(input);
|
||||||
|
const download_dir = input;
|
||||||
|
|
||||||
|
let cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'autostart-div',
|
||||||
|
'Start when added'
|
||||||
|
);
|
||||||
|
cal.check.dataset.key = 'start-added-torrents';
|
||||||
|
root.append(cal.root);
|
||||||
|
const autostart_check = cal.check;
|
||||||
|
|
||||||
|
cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'suffix-div',
|
||||||
|
`Append "part" to incomplete files' names`
|
||||||
|
);
|
||||||
|
cal.check.dataset.key = 'rename-partial-files';
|
||||||
|
root.append(cal.root);
|
||||||
|
const suffix_check = cal.check;
|
||||||
|
|
||||||
|
label = document.createElement('div');
|
||||||
|
label.textContent = 'Seeding';
|
||||||
|
label.classList.add('section-label');
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'stop-ratio-div',
|
||||||
|
'Stop seeding at ratio:'
|
||||||
|
);
|
||||||
|
cal.check.dataset.key = 'seedRatioLimited';
|
||||||
|
root.append(cal.root);
|
||||||
|
const stop_ratio_check = cal.check;
|
||||||
|
|
||||||
|
input = document.createElement('input');
|
||||||
|
input.type = 'number';
|
||||||
|
input.dataset.key = 'seedRatioLimit';
|
||||||
|
root.append(input);
|
||||||
|
PrefsDialog._enableIfChecked(input, cal.check);
|
||||||
|
const stop_ratio_input = input;
|
||||||
|
|
||||||
|
cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'stop-idle-div',
|
||||||
|
'Stop seeding if idle for N mins:'
|
||||||
|
);
|
||||||
|
cal.check.dataset.key = 'idle-seeding-limit-enabled';
|
||||||
|
root.append(cal.root);
|
||||||
|
const stop_idle_check = cal.check;
|
||||||
|
|
||||||
|
input = document.createElement('input');
|
||||||
|
input.type = 'number';
|
||||||
|
input.dataset.key = 'idle-seeding-limit';
|
||||||
|
root.append(input);
|
||||||
|
PrefsDialog._enableIfChecked(input, cal.check);
|
||||||
|
const stop_idle_input = input;
|
||||||
|
|
||||||
|
return {
|
||||||
|
autostart_check,
|
||||||
|
download_dir,
|
||||||
|
root,
|
||||||
|
stop_idle_check,
|
||||||
|
stop_idle_input,
|
||||||
|
stop_ratio_check,
|
||||||
|
stop_ratio_input,
|
||||||
|
suffix_check,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static _createSpeedPage() {
|
||||||
|
const root = document.createElement('div');
|
||||||
|
root.classList.add('prefs-speed-page');
|
||||||
|
|
||||||
|
let label = document.createElement('div');
|
||||||
|
label.textContent = 'Speed Limits';
|
||||||
|
label.classList.add('section-label');
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
let cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'upload-speed-div',
|
||||||
|
'Upload (kB/s):'
|
||||||
|
);
|
||||||
|
cal.check.dataset.key = 'speed-limit-up-enabled';
|
||||||
|
root.append(cal.root);
|
||||||
|
const upload_speed_check = cal.check;
|
||||||
|
|
||||||
|
let input = document.createElement('input');
|
||||||
|
input.type = 'number';
|
||||||
|
input.dataset.key = 'speed-limit-up';
|
||||||
|
root.append(input);
|
||||||
|
PrefsDialog._enableIfChecked(input, cal.check);
|
||||||
|
const upload_speed_input = input;
|
||||||
|
|
||||||
|
cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'download-speed-div',
|
||||||
|
'Download (kB/s):'
|
||||||
|
);
|
||||||
|
cal.check.dataset.key = 'speed-limit-down-enabled';
|
||||||
|
root.append(cal.root);
|
||||||
|
const download_speed_check = cal.check;
|
||||||
|
|
||||||
|
input = document.createElement('input');
|
||||||
|
input.type = 'number';
|
||||||
|
input.dataset.key = 'speed-limit-down';
|
||||||
|
root.append(input);
|
||||||
|
PrefsDialog._enableIfChecked(input, cal.check);
|
||||||
|
const download_speed_input = input;
|
||||||
|
|
||||||
|
label = document.createElement('div');
|
||||||
|
label.textContent = 'Alternative Speed Limits';
|
||||||
|
label.classList.add('section-label', 'alt-speed-section-label');
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
label = document.createElement('div');
|
||||||
|
label.textContent =
|
||||||
|
'Override normal speed limits manually or at scheduled times';
|
||||||
|
label.classList.add('alt-speed-label');
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.textContent = 'Upload (kB/s):';
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
input = document.createElement('input');
|
||||||
|
input.type = 'number';
|
||||||
|
input.dataset.key = 'alt-speed-up';
|
||||||
|
input.id = makeUUID();
|
||||||
|
label.setAttribute('for', input.id);
|
||||||
|
root.append(input);
|
||||||
|
const alt_upload_speed_input = input;
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.textContent = 'Download (kB/s):';
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
input = document.createElement('input');
|
||||||
|
input.type = 'number';
|
||||||
|
input.dataset.key = 'alt-speed-down';
|
||||||
|
input.id = makeUUID();
|
||||||
|
label.setAttribute('for', input.id);
|
||||||
|
root.append(input);
|
||||||
|
const alt_download_speed_input = input;
|
||||||
|
|
||||||
|
cal = PrefsDialog._createCheckAndLabel('alt-times-div', 'Scheduled times');
|
||||||
|
cal.check.dataset.key = 'alt-speed-time-enabled';
|
||||||
|
root.append(cal.root);
|
||||||
|
const alt_times_check = cal.check;
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.textContent = 'From:';
|
||||||
|
PrefsDialog._enableIfChecked(label, cal.check);
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
let select = document.createElement('select');
|
||||||
|
select.id = makeUUID();
|
||||||
|
select.dataset.key = 'alt-speed-time-begin';
|
||||||
|
PrefsDialog._initTimeDropDown(select);
|
||||||
|
label.setAttribute('for', select.id);
|
||||||
|
root.append(select);
|
||||||
|
PrefsDialog._enableIfChecked(select, cal.check);
|
||||||
|
const alt_from_select = select;
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.textContent = 'To:';
|
||||||
|
PrefsDialog._enableIfChecked(label, cal.check);
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
select = document.createElement('select');
|
||||||
|
select.id = makeUUID();
|
||||||
|
select.dataset.key = 'alt-speed-time-end';
|
||||||
|
PrefsDialog._initTimeDropDown(select);
|
||||||
|
label.setAttribute('for', select.id);
|
||||||
|
root.append(select);
|
||||||
|
PrefsDialog._enableIfChecked(select, cal.check);
|
||||||
|
const alt_to_select = select;
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.textContent = 'On days:';
|
||||||
|
PrefsDialog._enableIfChecked(label, cal.check);
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
select = document.createElement('select');
|
||||||
|
select.id = makeUUID();
|
||||||
|
select.dataset.key = 'alt-speed-time-day';
|
||||||
|
PrefsDialog._initDayDropDown(select);
|
||||||
|
label.setAttribute('for', select.id);
|
||||||
|
root.append(select);
|
||||||
|
PrefsDialog._enableIfChecked(select, cal.check);
|
||||||
|
const alt_days_select = select;
|
||||||
|
|
||||||
|
return {
|
||||||
|
alt_days_select,
|
||||||
|
alt_download_speed_input,
|
||||||
|
alt_from_select,
|
||||||
|
alt_times_check,
|
||||||
|
alt_to_select,
|
||||||
|
alt_upload_speed_input,
|
||||||
|
download_speed_check,
|
||||||
|
download_speed_input,
|
||||||
|
root,
|
||||||
|
upload_speed_check,
|
||||||
|
upload_speed_input,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static _createPeersPage() {
|
||||||
|
const root = document.createElement('div');
|
||||||
|
root.classList.add('prefs-peers-page');
|
||||||
|
|
||||||
|
let label = document.createElement('div');
|
||||||
|
label.textContent = 'Connections';
|
||||||
|
label.classList.add('section-label');
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
let cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'max-peers-per-torrent-div',
|
||||||
|
'Max peers per torrent:'
|
||||||
|
);
|
||||||
|
root.append(cal.root);
|
||||||
|
const max_peers_per_torrent_check = cal.check;
|
||||||
|
|
||||||
|
let input = document.createElement('input');
|
||||||
|
input.type = 'number';
|
||||||
|
input.dataset.key = 'peer-limit-per-torrent';
|
||||||
|
root.append(input);
|
||||||
|
PrefsDialog._enableIfChecked(input, cal.check);
|
||||||
|
const max_peers_per_torrent_input = input;
|
||||||
|
|
||||||
|
cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'max-peers-overall-div',
|
||||||
|
'Max peers overall:'
|
||||||
|
);
|
||||||
|
root.append(cal.root);
|
||||||
|
const max_peers_overall_check = cal.check;
|
||||||
|
|
||||||
|
input = document.createElement('input');
|
||||||
|
input.type = 'number';
|
||||||
|
input.dataset.key = 'peer-limit-global';
|
||||||
|
root.append(input);
|
||||||
|
PrefsDialog._enableIfChecked(input, cal.check);
|
||||||
|
const max_peers_overall_input = input;
|
||||||
|
|
||||||
|
label = document.createElement('div');
|
||||||
|
label.textContent = 'Options';
|
||||||
|
label.classList.add('section-label');
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.textContent = 'Encryption mode:';
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.id = makeUUID();
|
||||||
|
select.dataset.key = 'encryption';
|
||||||
|
select.options[0] = new Option('Prefer encryption', 'preferred');
|
||||||
|
select.options[1] = new Option('Allow encryption', 'tolerated');
|
||||||
|
select.options[2] = new Option('Require encryption', 'required');
|
||||||
|
root.append(select);
|
||||||
|
const encryption_select = select;
|
||||||
|
|
||||||
|
cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'use-pex-div',
|
||||||
|
'Use PEX to find more peers'
|
||||||
|
);
|
||||||
|
cal.check.title =
|
||||||
|
"PEX is a tool for exchanging peer lists with the peers you're connected to.";
|
||||||
|
cal.check.dataset.key = 'pex-enabled';
|
||||||
|
cal.label.title = cal.check.title;
|
||||||
|
root.append(cal.root);
|
||||||
|
const pex_check = cal.check;
|
||||||
|
|
||||||
|
cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'use-dht-div',
|
||||||
|
'Use DHT to find more peers'
|
||||||
|
);
|
||||||
|
cal.check.title = 'DHT is a tool for finding peers without a tracker.';
|
||||||
|
cal.check.dataset.key = 'dht-enabled';
|
||||||
|
cal.label.title = cal.check.title;
|
||||||
|
root.append(cal.root);
|
||||||
|
const dht_check = cal.check;
|
||||||
|
|
||||||
|
cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'use-lpd-div',
|
||||||
|
'Use LPD to find more peers'
|
||||||
|
);
|
||||||
|
cal.check.title = 'LPD is a tool for finding peers on your local network.';
|
||||||
|
cal.check.dataset.key = 'lpd-enabled';
|
||||||
|
cal.label.title = cal.check.title;
|
||||||
|
root.append(cal.root);
|
||||||
|
const lpd_check = cal.check;
|
||||||
|
|
||||||
|
label = document.createElement('div');
|
||||||
|
label.textContent = 'Blocklist';
|
||||||
|
label.classList.add('section-label');
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'blocklist-enabled-div',
|
||||||
|
'Enable blocklist:'
|
||||||
|
);
|
||||||
|
cal.check.dataset.key = 'blocklist-enabled';
|
||||||
|
root.append(cal.root);
|
||||||
|
const blocklist_enabled_check = cal.check;
|
||||||
|
|
||||||
|
input = document.createElement('input');
|
||||||
|
input.type = 'url';
|
||||||
|
input.value = 'http://www.example.com/blocklist';
|
||||||
|
input.dataset.key = 'blocklist-url';
|
||||||
|
root.append(input);
|
||||||
|
PrefsDialog._enableIfChecked(input, cal.check);
|
||||||
|
const blocklist_url_input = input;
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.textContent = 'Blocklist has {n} rules';
|
||||||
|
label.dataset.key = 'blocklist-size';
|
||||||
|
label.classList.add('blocklist-size-label');
|
||||||
|
PrefsDialog._enableIfChecked(label, cal.check);
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.classList.add('blocklist-update-button');
|
||||||
|
button.textContent = 'Update';
|
||||||
|
root.append(button);
|
||||||
|
PrefsDialog._enableIfChecked(button, cal.check);
|
||||||
|
const blocklist_update_button = button;
|
||||||
|
|
||||||
|
return {
|
||||||
|
blocklist_enabled_check,
|
||||||
|
blocklist_update_button,
|
||||||
|
blocklist_url_input,
|
||||||
|
dht_check,
|
||||||
|
encryption_select,
|
||||||
|
lpd_check,
|
||||||
|
max_peers_overall_check,
|
||||||
|
max_peers_overall_input,
|
||||||
|
max_peers_per_torrent_check,
|
||||||
|
max_peers_per_torrent_input,
|
||||||
|
pex_check,
|
||||||
|
root,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static _createNetworkPage() {
|
||||||
|
const root = document.createElement('div');
|
||||||
|
root.classList.add('prefs-network-page');
|
||||||
|
|
||||||
|
let label = document.createElement('div');
|
||||||
|
label.textContent = 'Listening Port';
|
||||||
|
label.classList.add('section-label');
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.textContent = 'Peer listening port:';
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'number';
|
||||||
|
input.dataset.key = 'peer-port';
|
||||||
|
input.id = makeUUID();
|
||||||
|
label.setAttribute('for', input.id);
|
||||||
|
root.append(input);
|
||||||
|
const port_input = input;
|
||||||
|
|
||||||
|
const port_status_div = document.createElement('div');
|
||||||
|
port_status_div.classList.add('port-status');
|
||||||
|
label = document.createElement('label');
|
||||||
|
label.textContent = 'Port is';
|
||||||
|
port_status_div.append(label);
|
||||||
|
const port_status_label = document.createElement('label');
|
||||||
|
port_status_label.textContent = '?';
|
||||||
|
port_status_label.classList.add('port-status-label');
|
||||||
|
port_status_div.append(port_status_label);
|
||||||
|
root.append(port_status_div);
|
||||||
|
|
||||||
|
let cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'randomize-port',
|
||||||
|
'Randomize port on launch'
|
||||||
|
);
|
||||||
|
cal.check.dataset.key = 'peer-port-random-on-start';
|
||||||
|
root.append(cal.root);
|
||||||
|
const random_port_check = cal.check;
|
||||||
|
|
||||||
|
cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'port-forwarding',
|
||||||
|
'Use port forwarding from my router'
|
||||||
|
);
|
||||||
|
cal.check.dataset.key = 'port-forwarding-enabled';
|
||||||
|
root.append(cal.root);
|
||||||
|
const port_forwarding_check = cal.check;
|
||||||
|
|
||||||
|
label = document.createElement('div');
|
||||||
|
label.textContent = 'Options';
|
||||||
|
label.classList.add('section-label');
|
||||||
|
root.append(label);
|
||||||
|
|
||||||
|
cal = PrefsDialog._createCheckAndLabel(
|
||||||
|
'utp-enabled',
|
||||||
|
'Enable uTP for peer communication'
|
||||||
|
);
|
||||||
|
cal.check.dataset.key = 'utp-enabled';
|
||||||
|
root.append(cal.root);
|
||||||
|
const utp_check = cal.check;
|
||||||
|
|
||||||
|
return {
|
||||||
|
port_forwarding_check,
|
||||||
|
port_input,
|
||||||
|
port_status_label,
|
||||||
|
random_port_check,
|
||||||
|
root,
|
||||||
|
utp_check,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static _create() {
|
||||||
|
const pages = {
|
||||||
|
network: PrefsDialog._createNetworkPage(),
|
||||||
|
peers: PrefsDialog._createPeersPage(),
|
||||||
|
speed: PrefsDialog._createSpeedPage(),
|
||||||
|
torrents: PrefsDialog._createTorrentsPage(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const elements = createTabsContainer('prefs-dialog', [
|
||||||
|
['prefs-tab-torrent', pages.torrents.root],
|
||||||
|
['prefs-tab-speed', pages.speed.root],
|
||||||
|
['prefs-tab-peers', pages.peers.root],
|
||||||
|
['prefs-tab-network', pages.network.root],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { ...elements, ...pages };
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(session_manager, remote) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.closed = false;
|
||||||
|
this.session_manager = session_manager;
|
||||||
|
this.remote = remote;
|
||||||
|
this.update_soon = () =>
|
||||||
|
this._update(this.session_manager.session_properties);
|
||||||
|
|
||||||
|
this.elements = PrefsDialog._create();
|
||||||
|
this.elements.peers.blocklist_update_button.addEventListener(
|
||||||
|
'click',
|
||||||
|
(event_) => {
|
||||||
|
setTextContent(event_.target, 'Updating blocklist...');
|
||||||
|
this.remote.updateBlocklist();
|
||||||
|
this._setBlocklistButtonEnabled(false);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.outside = new OutsideClickListener(this.elements.root);
|
||||||
|
this.outside.addEventListener('click', () => this.close());
|
||||||
|
|
||||||
|
Object.seal(this);
|
||||||
|
|
||||||
|
// listen for user input
|
||||||
|
const on_change = this._onControlChanged.bind(this);
|
||||||
|
const walk = (o) => {
|
||||||
|
for (const element of Object.values(o)) {
|
||||||
|
if (element.tagName === 'INPUT') {
|
||||||
|
switch (element.type) {
|
||||||
|
case 'checkbox':
|
||||||
|
case 'radio':
|
||||||
|
case 'number':
|
||||||
|
case 'text':
|
||||||
|
case 'url':
|
||||||
|
element.addEventListener('change', on_change);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.trace(`unhandled input: ${element.type}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(this.elements.network);
|
||||||
|
walk(this.elements.peers);
|
||||||
|
walk(this.elements.speed);
|
||||||
|
walk(this.elements.torrents);
|
||||||
|
|
||||||
|
this.session_manager.addEventListener('session-change', this.update_soon);
|
||||||
|
this.update_soon();
|
||||||
|
|
||||||
|
document.body.append(this.elements.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.closed) {
|
||||||
|
this.outside.stop();
|
||||||
|
this.session_manager.removeEventListener(
|
||||||
|
'session-change',
|
||||||
|
this.update_soon
|
||||||
|
);
|
||||||
|
this.elements.root.remove();
|
||||||
|
dispatchEvent(new Event('close'));
|
||||||
|
for (const key of Object.keys(this)) {
|
||||||
|
this[key] = null;
|
||||||
|
}
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { debounce } from './utils.js';
|
||||||
|
|
||||||
|
export class Prefs extends EventTarget {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._cache = {};
|
||||||
|
|
||||||
|
this.dispatchPrefsChange = debounce((key, old_value, value) => {
|
||||||
|
const event = new Event('change');
|
||||||
|
Object.assign(event, { key, old_value, value });
|
||||||
|
this.dispatchEvent(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [key, default_value] of Object.entries(Prefs._Defaults)) {
|
||||||
|
// populate the cache...
|
||||||
|
this._set(key, Prefs._getCookie(key, default_value));
|
||||||
|
|
||||||
|
// add property getter/setters...
|
||||||
|
Object.defineProperty(this, key.replaceAll('-', '_'), {
|
||||||
|
get: () => this._get(key),
|
||||||
|
set: (value) => {
|
||||||
|
this._set(key, value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.seal(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
entries() {
|
||||||
|
return Object.entries(this._cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
keys() {
|
||||||
|
return Object.keys(this._cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
_get(key) {
|
||||||
|
const { _cache } = this;
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(_cache, key)) {
|
||||||
|
throw new Error(key);
|
||||||
|
}
|
||||||
|
return _cache[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
_set(key, value) {
|
||||||
|
const { _cache } = this;
|
||||||
|
const old_value = _cache[key];
|
||||||
|
if (old_value !== value) {
|
||||||
|
_cache[key] = value;
|
||||||
|
Prefs._setCookie(key, value);
|
||||||
|
this.dispatchPrefsChange(key, old_value, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static _setCookie(key, value) {
|
||||||
|
const date = new Date();
|
||||||
|
date.setFullYear(date.getFullYear() + 1);
|
||||||
|
document.cookie = `${key}=${value}; SameSite=Strict; expires=${date.toGMTString()}; path=/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static _getCookie(key, fallback) {
|
||||||
|
const value = Prefs._readCookie(key);
|
||||||
|
if (value === null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
if (value === 'true') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (value === 'false') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (value.match(/^\d+$/)) {
|
||||||
|
return Number.parseInt(value, 10);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static _readCookie(key) {
|
||||||
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${key}=`);
|
||||||
|
return parts.length === 2 ? parts.pop().split(';').shift() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Prefs.AltSpeedEnabled = 'alt-speed-enabled';
|
||||||
|
Prefs.DisplayCompact = 'compact';
|
||||||
|
Prefs.DisplayFull = 'full';
|
||||||
|
Prefs.DisplayMode = 'display-mode';
|
||||||
|
Prefs.FilterActive = 'active';
|
||||||
|
Prefs.FilterAll = 'all';
|
||||||
|
Prefs.FilterDownloading = 'downloading';
|
||||||
|
Prefs.FilterFinished = 'finished';
|
||||||
|
Prefs.FilterMode = 'filter-mode';
|
||||||
|
Prefs.FilterPaused = 'paused';
|
||||||
|
Prefs.FilterSeeding = 'seeding';
|
||||||
|
Prefs.NotificationsEnabled = 'notifications-enabled';
|
||||||
|
Prefs.RefreshRate = 'refresh-rate-sec';
|
||||||
|
Prefs.SortAscending = 'ascending';
|
||||||
|
Prefs.SortByActivity = 'activity';
|
||||||
|
Prefs.SortByAge = 'age';
|
||||||
|
Prefs.SortByName = 'name';
|
||||||
|
Prefs.SortByProgress = 'progress';
|
||||||
|
Prefs.SortByQueue = 'queue';
|
||||||
|
Prefs.SortByRatio = 'ratio';
|
||||||
|
Prefs.SortBySize = 'size';
|
||||||
|
Prefs.SortByState = 'state';
|
||||||
|
Prefs.SortDescending = 'descending';
|
||||||
|
Prefs.SortDirection = 'sort-direction';
|
||||||
|
Prefs.SortMode = 'sort-mode';
|
||||||
|
|
||||||
|
Prefs._Defaults = {
|
||||||
|
[Prefs.AltSpeedEnabled]: false,
|
||||||
|
[Prefs.DisplayMode]: Prefs.DisplayFull,
|
||||||
|
[Prefs.FilterMode]: Prefs.FilterAll,
|
||||||
|
[Prefs.NotificationsEnabled]: false,
|
||||||
|
[Prefs.RefreshRate]: 5,
|
||||||
|
[Prefs.SortDirection]: Prefs.SortAscending,
|
||||||
|
[Prefs.SortMode]: Prefs.SortByName,
|
||||||
|
};
|
|
@ -0,0 +1,314 @@
|
||||||
|
/**
|
||||||
|
* Copyright © Charles Kerr, Dave Perrett, Malcolm Jarvis and Bruno Bierbaumer
|
||||||
|
*
|
||||||
|
* This file is licensed under the GPLv2.
|
||||||
|
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AlertDialog } from './alert-dialog.js';
|
||||||
|
|
||||||
|
export const RPC = {
|
||||||
|
_DaemonVersion: 'version',
|
||||||
|
_DownSpeedLimit: 'speed-limit-down',
|
||||||
|
_DownSpeedLimited: 'speed-limit-down-enabled',
|
||||||
|
_QueueMoveBottom: 'queue-move-bottom',
|
||||||
|
_QueueMoveDown: 'queue-move-down',
|
||||||
|
_QueueMoveTop: 'queue-move-top',
|
||||||
|
_QueueMoveUp: 'queue-move-up',
|
||||||
|
_Root: '../rpc',
|
||||||
|
_TurtleDownSpeedLimit: 'alt-speed-down',
|
||||||
|
_TurtleState: 'alt-speed-enabled',
|
||||||
|
_TurtleUpSpeedLimit: 'alt-speed-up',
|
||||||
|
_UpSpeedLimit: 'speed-limit-up',
|
||||||
|
_UpSpeedLimited: 'speed-limit-up-enabled',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Remote {
|
||||||
|
// TODO: decouple from controller
|
||||||
|
constructor(controller) {
|
||||||
|
this._controller = controller;
|
||||||
|
this._error = '';
|
||||||
|
this._session_id = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRequest(data, callback, context) {
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.append('cache-control', 'no-cache');
|
||||||
|
headers.append('content-type', 'application/json');
|
||||||
|
headers.append('pragma', 'no-cache');
|
||||||
|
if (this._session_id) {
|
||||||
|
headers.append(Remote._SessionHeader, this._session_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_argument = null;
|
||||||
|
fetch(RPC._Root, {
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
response_argument = response;
|
||||||
|
if (response.status === 409) {
|
||||||
|
const error = new Error(Remote._SessionHeader);
|
||||||
|
error.header = response.headers.get(Remote._SessionHeader);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((payload) => {
|
||||||
|
if (callback) {
|
||||||
|
callback.call(context, payload, response_argument);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.message === Remote._SessionHeader) {
|
||||||
|
// copy the session header and try again
|
||||||
|
this._session_id = error.header;
|
||||||
|
this.sendRequest(data, callback, context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.trace(error);
|
||||||
|
this._controller.togglePeriodicSessionRefresh(false);
|
||||||
|
this._controller.setCurrentPopup(
|
||||||
|
new AlertDialog({
|
||||||
|
heading: 'Connection failed',
|
||||||
|
message:
|
||||||
|
'Could not connect to the server. You may need to reload the page to reconnect.',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: return a Promise
|
||||||
|
loadDaemonPrefs(callback, context) {
|
||||||
|
const o = {
|
||||||
|
method: 'session-get',
|
||||||
|
};
|
||||||
|
this.sendRequest(o, callback, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPort(callback, context) {
|
||||||
|
const o = {
|
||||||
|
method: 'port-test',
|
||||||
|
};
|
||||||
|
this.sendRequest(o, callback, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
renameTorrent(torrentIds, oldpath, newname, callback, context) {
|
||||||
|
const o = {
|
||||||
|
arguments: {
|
||||||
|
ids: torrentIds,
|
||||||
|
name: newname,
|
||||||
|
path: oldpath,
|
||||||
|
},
|
||||||
|
method: 'torrent-rename-path',
|
||||||
|
};
|
||||||
|
this.sendRequest(o, callback, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDaemonStats(callback, context) {
|
||||||
|
const o = {
|
||||||
|
method: 'session-stats',
|
||||||
|
};
|
||||||
|
this.sendRequest(o, callback, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTorrents(torrentIds, fields, callback, context) {
|
||||||
|
const o = {
|
||||||
|
arguments: {
|
||||||
|
fields,
|
||||||
|
format: 'table',
|
||||||
|
},
|
||||||
|
method: 'torrent-get',
|
||||||
|
};
|
||||||
|
if (torrentIds) {
|
||||||
|
o.arguments.ids = torrentIds;
|
||||||
|
}
|
||||||
|
this.sendRequest(o, (response) => {
|
||||||
|
const arguments_ = response['arguments'];
|
||||||
|
callback.call(context, arguments_.torrents, arguments_.removed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getFreeSpace(dir, callback, context) {
|
||||||
|
const o = {
|
||||||
|
arguments: {
|
||||||
|
path: dir,
|
||||||
|
},
|
||||||
|
method: 'free-space',
|
||||||
|
};
|
||||||
|
this.sendRequest(o, (response) => {
|
||||||
|
const arguments_ = response['arguments'];
|
||||||
|
callback.call(context, arguments_.path, arguments_['size-bytes']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeFileCommand(torrentId, fileIndices, command) {
|
||||||
|
const arguments_ = {
|
||||||
|
ids: [torrentId],
|
||||||
|
};
|
||||||
|
arguments_[command] = fileIndices;
|
||||||
|
this.sendRequest(
|
||||||
|
{
|
||||||
|
arguments: arguments_,
|
||||||
|
method: 'torrent-set',
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this._controller.refreshTorrents([torrentId]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTorrentSetRequests(method, torrent_ids, arguments_, callback, context) {
|
||||||
|
if (!arguments_) {
|
||||||
|
arguments_ = {};
|
||||||
|
}
|
||||||
|
arguments_['ids'] = torrent_ids;
|
||||||
|
const o = {
|
||||||
|
arguments: arguments_,
|
||||||
|
method,
|
||||||
|
};
|
||||||
|
this.sendRequest(o, callback, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTorrentActionRequests(method, torrent_ids, callback, context) {
|
||||||
|
this.sendTorrentSetRequests(method, torrent_ids, null, callback, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
startTorrents(torrent_ids, noqueue, callback, context) {
|
||||||
|
const name = noqueue ? 'torrent-start-now' : 'torrent-start';
|
||||||
|
this.sendTorrentActionRequests(name, torrent_ids, callback, context);
|
||||||
|
}
|
||||||
|
stopTorrents(torrent_ids, callback, context) {
|
||||||
|
this.sendTorrentActionRequests(
|
||||||
|
'torrent-stop',
|
||||||
|
torrent_ids,
|
||||||
|
callback,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveTorrents(torrent_ids, new_location, callback, context) {
|
||||||
|
this.sendTorrentSetRequests(
|
||||||
|
'torrent-set-location',
|
||||||
|
torrent_ids,
|
||||||
|
{
|
||||||
|
location: new_location,
|
||||||
|
move: true,
|
||||||
|
},
|
||||||
|
callback,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTorrents(torrent_ids, callback, context) {
|
||||||
|
this.sendTorrentActionRequests(
|
||||||
|
'torrent-remove',
|
||||||
|
torrent_ids,
|
||||||
|
callback,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
}
|
||||||
|
removeTorrentsAndData(torrents) {
|
||||||
|
const o = {
|
||||||
|
arguments: {
|
||||||
|
'delete-local-data': true,
|
||||||
|
ids: [],
|
||||||
|
},
|
||||||
|
method: 'torrent-remove',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (torrents) {
|
||||||
|
for (let index = 0, length_ = torrents.length; index < length_; ++index) {
|
||||||
|
o.arguments.ids.push(torrents[index].getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.sendRequest(o, () => {
|
||||||
|
this._controller.refreshTorrents();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
verifyTorrents(torrent_ids, callback, context) {
|
||||||
|
this.sendTorrentActionRequests(
|
||||||
|
'torrent-verify',
|
||||||
|
torrent_ids,
|
||||||
|
callback,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
}
|
||||||
|
reannounceTorrents(torrent_ids, callback, context) {
|
||||||
|
this.sendTorrentActionRequests(
|
||||||
|
'torrent-reannounce',
|
||||||
|
torrent_ids,
|
||||||
|
callback,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
}
|
||||||
|
addTorrentByUrl(url, options) {
|
||||||
|
if (url.match(/^[\da-f]{40}$/i)) {
|
||||||
|
url = `magnet:?xt=urn:btih:${url}`;
|
||||||
|
}
|
||||||
|
const o = {
|
||||||
|
arguments: {
|
||||||
|
filename: url,
|
||||||
|
paused: options.paused,
|
||||||
|
},
|
||||||
|
method: 'torrent-add',
|
||||||
|
};
|
||||||
|
this.sendRequest(o, () => {
|
||||||
|
this._controller.refreshTorrents();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
savePrefs(arguments_) {
|
||||||
|
const o = {
|
||||||
|
arguments: arguments_,
|
||||||
|
method: 'session-set',
|
||||||
|
};
|
||||||
|
this.sendRequest(o, () => {
|
||||||
|
this._controller.loadDaemonPrefs();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateBlocklist() {
|
||||||
|
const o = {
|
||||||
|
method: 'blocklist-update',
|
||||||
|
};
|
||||||
|
this.sendRequest(o, () => {
|
||||||
|
this._controller.loadDaemonPrefs();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Added queue calls
|
||||||
|
moveTorrentsToTop(torrent_ids, callback, context) {
|
||||||
|
this.sendTorrentActionRequests(
|
||||||
|
RPC._QueueMoveTop,
|
||||||
|
torrent_ids,
|
||||||
|
callback,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
}
|
||||||
|
moveTorrentsToBottom(torrent_ids, callback, context) {
|
||||||
|
this.sendTorrentActionRequests(
|
||||||
|
RPC._QueueMoveBottom,
|
||||||
|
torrent_ids,
|
||||||
|
callback,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
}
|
||||||
|
moveTorrentsUp(torrent_ids, callback, context) {
|
||||||
|
this.sendTorrentActionRequests(
|
||||||
|
RPC._QueueMoveUp,
|
||||||
|
torrent_ids,
|
||||||
|
callback,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
}
|
||||||
|
moveTorrentsDown(torrent_ids, callback, context) {
|
||||||
|
this.sendTorrentActionRequests(
|
||||||
|
RPC._QueueMoveDown,
|
||||||
|
torrent_ids,
|
||||||
|
callback,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Remote._SessionHeader = 'X-Transmission-Session-Id';
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createDialogContainer } from './utils.js';
|
||||||
|
|
||||||
|
export class RemoveDialog extends EventTarget {
|
||||||
|
constructor(options) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// options: remote, torrents, trash
|
||||||
|
this.options = options;
|
||||||
|
this.elements = RemoveDialog._create(options);
|
||||||
|
this.elements.dismiss.addEventListener('click', () => this._onDismiss());
|
||||||
|
this.elements.confirm.addEventListener('click', () => this._onConfirm());
|
||||||
|
document.body.append(this.elements.root);
|
||||||
|
this.elements.dismiss.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.closed) {
|
||||||
|
this.elements.root.remove();
|
||||||
|
this.dispatchEvent(new Event('close'));
|
||||||
|
for (const key of Object.keys(this)) {
|
||||||
|
delete this[key];
|
||||||
|
}
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDismiss() {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onConfirm() {
|
||||||
|
const { torrents } = this.options;
|
||||||
|
if (torrents.length > 0) {
|
||||||
|
if (this.options.trash) {
|
||||||
|
this.options.remote.removeTorrentsAndData(torrents);
|
||||||
|
} else {
|
||||||
|
this.options.remote.removeTorrents(torrents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static _create(options) {
|
||||||
|
const { trash } = options;
|
||||||
|
const { heading, message } = RemoveDialog._createMessage(options);
|
||||||
|
|
||||||
|
const elements = createDialogContainer('remove-dialog');
|
||||||
|
elements.heading.textContent = heading;
|
||||||
|
elements.message.textContent = message;
|
||||||
|
elements.confirm.textContent = trash ? 'Trash' : 'Remove';
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
static _createMessage(options) {
|
||||||
|
let heading = null;
|
||||||
|
let message = null;
|
||||||
|
const { torrents } = options;
|
||||||
|
const [torrent] = torrents;
|
||||||
|
if (options.trash && torrents.length === 1) {
|
||||||
|
heading = `Remove ${torrent.getName()} and delete data?`;
|
||||||
|
message =
|
||||||
|
'All data downloaded for this torrent will be deleted. Are you sure you want to remove it?';
|
||||||
|
} else if (options.trash) {
|
||||||
|
heading = `Remove ${torrents.length} transfers and delete data?`;
|
||||||
|
message =
|
||||||
|
'All data downloaded for these torrents will be deleted. Are you sure you want to remove them?';
|
||||||
|
} else if (torrents.length === 1) {
|
||||||
|
heading = `Remove ${torrent.getName()}?`;
|
||||||
|
message =
|
||||||
|
'Once removed, continuing the transfer will require the torrent file. Are you sure you want to remove it?';
|
||||||
|
} else {
|
||||||
|
heading = `Remove ${torrents.length} transfers?`;
|
||||||
|
message =
|
||||||
|
'Once removed, continuing the transfers will require the torrent files. Are you sure you want to remove them?';
|
||||||
|
}
|
||||||
|
return { heading, message };
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createDialogContainer } from './utils.js';
|
||||||
|
|
||||||
|
export class RenameDialog extends EventTarget {
|
||||||
|
constructor(controller, remote) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.controller = controller;
|
||||||
|
this.remote = remote;
|
||||||
|
this.elements = {};
|
||||||
|
this.torrents = [];
|
||||||
|
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
const torrents = this.controller.getSelectedTorrents();
|
||||||
|
if (torrents.length !== 1) {
|
||||||
|
console.trace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.torrents = torrents;
|
||||||
|
this.elements = RenameDialog._create();
|
||||||
|
this.elements.entry.value = torrents[0].getName();
|
||||||
|
document.body.append(this.elements.root);
|
||||||
|
|
||||||
|
this.elements.entry.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.elements.root.remove();
|
||||||
|
|
||||||
|
this.dispatchEvent(new Event('close'));
|
||||||
|
|
||||||
|
delete this.controller;
|
||||||
|
delete this.remote;
|
||||||
|
delete this.elements;
|
||||||
|
delete this.torrents;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDismiss() {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onConfirm() {
|
||||||
|
const [tor] = this.torrents;
|
||||||
|
const old_name = tor.getName();
|
||||||
|
const new_name = this.elements.entry.value;
|
||||||
|
this.remote.renameTorrent([tor.getId()], old_name, new_name, (response) => {
|
||||||
|
if (response.result === 'success') {
|
||||||
|
tor.refresh(response.arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static _create() {
|
||||||
|
const elements = createDialogContainer('rename-dialog');
|
||||||
|
elements.root.setAttribute('aria-label', 'Rename Torrent');
|
||||||
|
elements.heading.textContent = 'Enter new name:';
|
||||||
|
elements.confirm.textContent = 'Rename';
|
||||||
|
elements.dismiss.addEventListener('click', () => this._onDismiss());
|
||||||
|
elements.confirm.addEventListener('click', () => this._onConfirm());
|
||||||
|
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.setAttribute('for', 'torrent-rename-name');
|
||||||
|
label.textContent = 'Enter new name:';
|
||||||
|
elements.workarea.append(label);
|
||||||
|
|
||||||
|
const entry = document.createElement('input');
|
||||||
|
entry.setAttribute('type', 'text');
|
||||||
|
entry.id = 'torrent-rename-name';
|
||||||
|
elements.entry = entry;
|
||||||
|
elements.workarea.append(entry);
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createDialogContainer } from './utils.js';
|
||||||
|
|
||||||
|
export class ShortcutsDialog extends EventTarget {
|
||||||
|
constructor(action_manager) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.elements = ShortcutsDialog._create(action_manager);
|
||||||
|
this.elements.dismiss.addEventListener('click', () => this._onDismiss());
|
||||||
|
document.body.append(this.elements.root);
|
||||||
|
this.elements.dismiss.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.elements.root.remove();
|
||||||
|
this.dispatchEvent(new Event('close'));
|
||||||
|
delete this.elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDismiss() {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static _create(action_manager) {
|
||||||
|
const elements = createDialogContainer('shortcuts-dialog');
|
||||||
|
elements.root.setAttribute('aria-label', 'Keyboard Shortcuts');
|
||||||
|
|
||||||
|
const table = document.createElement('table');
|
||||||
|
const thead = document.createElement('thead');
|
||||||
|
table.append(thead);
|
||||||
|
|
||||||
|
let tr = document.createElement('tr');
|
||||||
|
thead.append(tr);
|
||||||
|
|
||||||
|
let th = document.createElement('th');
|
||||||
|
th.textContent = 'Key';
|
||||||
|
tr.append(th);
|
||||||
|
|
||||||
|
th = document.createElement('th');
|
||||||
|
th.textContent = 'Action';
|
||||||
|
tr.append(th);
|
||||||
|
|
||||||
|
const tbody = document.createElement('tbody');
|
||||||
|
table.append(tbody);
|
||||||
|
|
||||||
|
const o = {};
|
||||||
|
for (const [shortcut, name] of action_manager.allShortcuts().entries()) {
|
||||||
|
const tokens = shortcut.split('+');
|
||||||
|
const sortKey = [tokens.pop(), ...tokens].join('+');
|
||||||
|
o[sortKey] = { name, shortcut };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [, values] of Object.entries(o).sort()) {
|
||||||
|
const { name, shortcut } = values;
|
||||||
|
tr = document.createElement('tr');
|
||||||
|
tbody.append(tr);
|
||||||
|
|
||||||
|
let td = document.createElement('td');
|
||||||
|
td.textContent = shortcut.replaceAll('+', ' + ');
|
||||||
|
tr.append(td);
|
||||||
|
|
||||||
|
td = document.createElement('td');
|
||||||
|
td.textContent = action_manager.text(name);
|
||||||
|
tr.append(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.heading.textContent = 'Transmission';
|
||||||
|
elements.dismiss.textContent = 'Close';
|
||||||
|
|
||||||
|
elements.heading.textContent = 'Keyboard shortcuts';
|
||||||
|
elements.message.append(table);
|
||||||
|
elements.confirm.remove();
|
||||||
|
delete elements.confirm;
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Formatter } from './formatter.js';
|
||||||
|
import {
|
||||||
|
Utils,
|
||||||
|
setTextContent,
|
||||||
|
createDialogContainer,
|
||||||
|
createInfoSection,
|
||||||
|
} from './utils.js';
|
||||||
|
|
||||||
|
export class StatisticsDialog extends EventTarget {
|
||||||
|
constructor(remote) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.remote = remote;
|
||||||
|
|
||||||
|
const updateDaemon = () =>
|
||||||
|
this.remote.loadDaemonStats((data) => this._update(data.arguments));
|
||||||
|
const delay_msec = 5000;
|
||||||
|
this.interval = setInterval(updateDaemon, delay_msec);
|
||||||
|
updateDaemon();
|
||||||
|
|
||||||
|
this.elements = StatisticsDialog._create();
|
||||||
|
this.elements.dismiss.addEventListener('click', () => this._onDismiss());
|
||||||
|
document.body.append(this.elements.root);
|
||||||
|
this.elements.dismiss.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.closed) {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
this.elements.root.remove();
|
||||||
|
for (const key of Object.keys(this)) {
|
||||||
|
delete this[key];
|
||||||
|
}
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDismiss() {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
_update(stats) {
|
||||||
|
console.log(stats);
|
||||||
|
const fmt = Formatter;
|
||||||
|
|
||||||
|
let s = stats['current-stats'];
|
||||||
|
let ratio = Utils.ratio(s.uploadedBytes, s.downloadedBytes);
|
||||||
|
setTextContent(this.elements.session.up, fmt.size(s.uploadedBytes));
|
||||||
|
setTextContent(this.elements.session.down, fmt.size(s.downloadedBytes));
|
||||||
|
setTextContent(this.elements.session.ratio, fmt.ratioString(ratio));
|
||||||
|
setTextContent(
|
||||||
|
this.elements.session.time,
|
||||||
|
fmt.timeInterval(s.secondsActive)
|
||||||
|
);
|
||||||
|
|
||||||
|
s = stats['cumulative-stats'];
|
||||||
|
ratio = Utils.ratio(s.uploadedBytes, s.downloadedBytes);
|
||||||
|
setTextContent(this.elements.total.up, fmt.size(s.uploadedBytes));
|
||||||
|
setTextContent(this.elements.total.down, fmt.size(s.downloadedBytes));
|
||||||
|
setTextContent(this.elements.total.ratio, fmt.ratioString(ratio));
|
||||||
|
setTextContent(this.elements.total.time, fmt.timeInterval(s.secondsActive));
|
||||||
|
}
|
||||||
|
|
||||||
|
static _create() {
|
||||||
|
const elements = createDialogContainer('statistics-dialog');
|
||||||
|
const { workarea } = elements;
|
||||||
|
elements.confirm.remove();
|
||||||
|
elements.dismiss.textContent = 'Close';
|
||||||
|
delete elements.confirm;
|
||||||
|
|
||||||
|
const heading_text = 'Statistics';
|
||||||
|
elements.root.setAttribute('aria-label', heading_text);
|
||||||
|
elements.heading.textContent = heading_text;
|
||||||
|
|
||||||
|
const labels = ['Uploaded:', 'Downloaded:', 'Ratio:', 'Running time:'];
|
||||||
|
let section = createInfoSection('Current session', labels);
|
||||||
|
const [sup, sdown, sratio, stime] = section.children;
|
||||||
|
const session = (elements.session = {});
|
||||||
|
session.up = sup;
|
||||||
|
session.down = sdown;
|
||||||
|
session.ratio = sratio;
|
||||||
|
session.time = stime;
|
||||||
|
workarea.append(section.root);
|
||||||
|
|
||||||
|
section = createInfoSection('Total', labels);
|
||||||
|
const [tup, tdown, tratio, ttime] = section.children;
|
||||||
|
const total = (elements.total = {});
|
||||||
|
total.up = tup;
|
||||||
|
total.down = tdown;
|
||||||
|
total.ratio = tratio;
|
||||||
|
total.time = ttime;
|
||||||
|
workarea.append(section.root);
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,408 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Formatter } from './formatter.js';
|
||||||
|
import { Torrent } from './torrent.js';
|
||||||
|
import { setTextContent } from './utils.js';
|
||||||
|
|
||||||
|
class TorrentRendererHelper {
|
||||||
|
static getProgressInfo(controller, t) {
|
||||||
|
const status = t.getStatus();
|
||||||
|
const classList = ['torrent-progress-bar'];
|
||||||
|
let percent = null;
|
||||||
|
|
||||||
|
if (status === Torrent._StatusStopped) {
|
||||||
|
classList.push('paused');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.needsMetaData()) {
|
||||||
|
classList.push('magnet');
|
||||||
|
percent = Math.round(t.getMetadataPercentComplete() * 100);
|
||||||
|
} else if (status === Torrent._StatusCheck) {
|
||||||
|
classList.push('verify');
|
||||||
|
percent = Math.round(t.getRecheckProgress() * 100);
|
||||||
|
} else if (t.getLeftUntilDone() > 0) {
|
||||||
|
classList.push('leech');
|
||||||
|
percent = Math.round(t.getPercentDone() * 100);
|
||||||
|
} else {
|
||||||
|
classList.push('seed');
|
||||||
|
const seed_ratio_limit = t.seedRatioLimit(controller);
|
||||||
|
percent =
|
||||||
|
seed_ratio_limit > 0
|
||||||
|
? (t.getUploadRatio() * 100) / seed_ratio_limit
|
||||||
|
: 100;
|
||||||
|
}
|
||||||
|
if (t.isQueued()) {
|
||||||
|
classList.push('queued');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
classList,
|
||||||
|
percent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static renderProgressbar(controller, t, progressbar) {
|
||||||
|
const info = TorrentRendererHelper.getProgressInfo(controller, t);
|
||||||
|
progressbar.className = info.classList.join(' ');
|
||||||
|
progressbar.style['background-size'] = `${info.percent}% 100%, 100% 100%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static formatUL(t) {
|
||||||
|
return `▲${Formatter.speedBps(t.getUploadSpeed())}`;
|
||||||
|
}
|
||||||
|
static formatDL(t) {
|
||||||
|
return `▼${Formatter.speedBps(t.getDownloadSpeed())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static formatETA(t) {
|
||||||
|
const eta = t.getETA();
|
||||||
|
if (eta < 0 || eta >= 999 * 60 * 60) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return `ETA: ${Formatter.timeInterval(eta)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
export class TorrentRendererFull {
|
||||||
|
static getPeerDetails(t) {
|
||||||
|
const fmt = Formatter;
|
||||||
|
|
||||||
|
const error = t.getErrorMessage();
|
||||||
|
if (error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.isDownloading()) {
|
||||||
|
const peer_count = t.getPeersConnected();
|
||||||
|
const webseed_count = t.getWebseedsSendingToUs();
|
||||||
|
|
||||||
|
if (webseed_count && peer_count) {
|
||||||
|
// Downloading from 2 of 3 peer(s) and 2 webseed(s)
|
||||||
|
return [
|
||||||
|
'Downloading from',
|
||||||
|
t.getPeersSendingToUs(),
|
||||||
|
'of',
|
||||||
|
fmt.countString('peer', 'peers', peer_count),
|
||||||
|
'and',
|
||||||
|
fmt.countString('web seed', 'web seeds', webseed_count),
|
||||||
|
'–',
|
||||||
|
TorrentRendererHelper.formatDL(t),
|
||||||
|
TorrentRendererHelper.formatUL(t),
|
||||||
|
].join(' ');
|
||||||
|
}
|
||||||
|
if (webseed_count) {
|
||||||
|
// Downloading from 2 webseed(s)
|
||||||
|
return [
|
||||||
|
'Downloading from',
|
||||||
|
fmt.countString('web seed', 'web seeds', webseed_count),
|
||||||
|
'–',
|
||||||
|
TorrentRendererHelper.formatDL(t),
|
||||||
|
TorrentRendererHelper.formatUL(t),
|
||||||
|
].join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downloading from 2 of 3 peer(s)
|
||||||
|
return [
|
||||||
|
'Downloading from',
|
||||||
|
t.getPeersSendingToUs(),
|
||||||
|
'of',
|
||||||
|
fmt.countString('peer', 'peers', peer_count),
|
||||||
|
'–',
|
||||||
|
TorrentRendererHelper.formatDL(t),
|
||||||
|
TorrentRendererHelper.formatUL(t),
|
||||||
|
].join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.isSeeding()) {
|
||||||
|
return [
|
||||||
|
'Seeding to',
|
||||||
|
t.getPeersGettingFromUs(),
|
||||||
|
'of',
|
||||||
|
fmt.countString('peer', 'peers', t.getPeersConnected()),
|
||||||
|
'-',
|
||||||
|
TorrentRendererHelper.formatUL(t),
|
||||||
|
].join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.isChecking()) {
|
||||||
|
return [
|
||||||
|
'Verifying local data (',
|
||||||
|
Formatter.percentString(100 * t.getRecheckProgress()),
|
||||||
|
'% tested)',
|
||||||
|
].join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.getStateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static getProgressDetails(controller, t) {
|
||||||
|
if (t.needsMetaData()) {
|
||||||
|
let MetaDataStatus = 'retrieving';
|
||||||
|
if (t.isStopped()) {
|
||||||
|
MetaDataStatus = 'needs';
|
||||||
|
}
|
||||||
|
const percent = 100 * t.getMetadataPercentComplete();
|
||||||
|
return [
|
||||||
|
`Magnetized transfer - ${MetaDataStatus} metadata (`,
|
||||||
|
Formatter.percentString(percent),
|
||||||
|
'%)',
|
||||||
|
].join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeWhenDone = t.getSizeWhenDone();
|
||||||
|
const totalSize = t.getTotalSize();
|
||||||
|
const is_done = t.isDone() || t.isSeeding();
|
||||||
|
const c = [];
|
||||||
|
|
||||||
|
if (is_done) {
|
||||||
|
if (totalSize === sizeWhenDone) {
|
||||||
|
// seed: '698.05 MiB'
|
||||||
|
c.push(Formatter.size(totalSize));
|
||||||
|
} else {
|
||||||
|
// partial seed: '127.21 MiB of 698.05 MiB (18.2%)'
|
||||||
|
c.push(
|
||||||
|
Formatter.size(sizeWhenDone),
|
||||||
|
' of ',
|
||||||
|
Formatter.size(t.getTotalSize()),
|
||||||
|
' (',
|
||||||
|
t.getPercentDoneStr(),
|
||||||
|
'%)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// append UL stats: ', uploaded 8.59 GiB (Ratio: 12.3)'
|
||||||
|
c.push(
|
||||||
|
', uploaded ',
|
||||||
|
Formatter.size(t.getUploadedEver()),
|
||||||
|
' (Ratio ',
|
||||||
|
Formatter.ratioString(t.getUploadRatio()),
|
||||||
|
')'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// not done yet
|
||||||
|
c.push(
|
||||||
|
Formatter.size(sizeWhenDone - t.getLeftUntilDone()),
|
||||||
|
' of ',
|
||||||
|
Formatter.size(sizeWhenDone),
|
||||||
|
' (',
|
||||||
|
t.getPercentDoneStr(),
|
||||||
|
'%)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybe append eta
|
||||||
|
if (!t.isStopped() && (!is_done || t.seedRatioLimit(controller) > 0)) {
|
||||||
|
c.push(' - ');
|
||||||
|
const eta = t.getETA();
|
||||||
|
if (eta < 0 || eta >= 999 * 60 * 60 /* arbitrary */) {
|
||||||
|
c.push('remaining time unknown');
|
||||||
|
} else {
|
||||||
|
c.push(Formatter.timeInterval(t.getETA()), ' remaining');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
render(controller, t, root) {
|
||||||
|
const is_stopped = t.isStopped();
|
||||||
|
|
||||||
|
// name
|
||||||
|
let e = root._name_container;
|
||||||
|
setTextContent(e, t.getName());
|
||||||
|
e.classList.toggle('paused', is_stopped);
|
||||||
|
|
||||||
|
// progressbar
|
||||||
|
TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
|
||||||
|
root._progressbar.classList.add('full');
|
||||||
|
|
||||||
|
// peer details
|
||||||
|
const has_error = t.getError() !== Torrent._ErrNone;
|
||||||
|
e = root._peer_details_container;
|
||||||
|
e.classList.toggle('error', has_error);
|
||||||
|
setTextContent(e, TorrentRendererFull.getPeerDetails(t));
|
||||||
|
|
||||||
|
// progress details
|
||||||
|
e = root._progress_details_container;
|
||||||
|
setTextContent(e, TorrentRendererFull.getProgressDetails(controller, t));
|
||||||
|
|
||||||
|
// pause/resume button
|
||||||
|
e = root._toggle_running_button;
|
||||||
|
e.alt = is_stopped ? 'Resume' : 'Pause';
|
||||||
|
e.dataset.action = is_stopped ? 'resume' : 'pause';
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
createRow(torrent) {
|
||||||
|
const root = document.createElement('li');
|
||||||
|
root.className = 'torrent';
|
||||||
|
|
||||||
|
const icon = document.createElement('div');
|
||||||
|
icon.classList.add('icon');
|
||||||
|
icon.dataset.iconMimeType = torrent
|
||||||
|
.getPrimaryMimeType()
|
||||||
|
.split('/', 1)
|
||||||
|
.pop();
|
||||||
|
icon.dataset.iconMultifile = torrent.getFileCount() > 1 ? 'true' : 'false';
|
||||||
|
|
||||||
|
const name = document.createElement('div');
|
||||||
|
name.className = 'torrent-name';
|
||||||
|
|
||||||
|
const peers = document.createElement('div');
|
||||||
|
peers.className = 'torrent-peer-details';
|
||||||
|
|
||||||
|
const progress = document.createElement('div');
|
||||||
|
progress.classList.add('torrent-progress');
|
||||||
|
const progressbar = document.createElement('div');
|
||||||
|
progressbar.classList.add('torrent-progress-bar', 'full');
|
||||||
|
progress.append(progressbar);
|
||||||
|
const button = document.createElement('a');
|
||||||
|
button.className = 'torrent-pauseresume-button';
|
||||||
|
progress.append(button);
|
||||||
|
|
||||||
|
const details = document.createElement('div');
|
||||||
|
details.className = 'torrent-progress-details';
|
||||||
|
|
||||||
|
root.append(icon);
|
||||||
|
root.append(name);
|
||||||
|
root.append(peers);
|
||||||
|
root.append(progress);
|
||||||
|
root.append(details);
|
||||||
|
|
||||||
|
root._icon = icon;
|
||||||
|
root._name_container = name;
|
||||||
|
root._peer_details_container = peers;
|
||||||
|
root._progress_details_container = details;
|
||||||
|
root._progressbar = progressbar;
|
||||||
|
root._toggle_running_button = button;
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
export class TorrentRendererCompact {
|
||||||
|
static getPeerDetails(t) {
|
||||||
|
const errorMessage = t.getErrorMessage();
|
||||||
|
if (errorMessage) {
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
if (t.isDownloading()) {
|
||||||
|
const have_dn = t.getDownloadSpeed() > 0;
|
||||||
|
const have_up = t.getUploadSpeed() > 0;
|
||||||
|
|
||||||
|
if (!have_up && !have_dn) {
|
||||||
|
return 'Idle';
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = [`${TorrentRendererHelper.formatETA(t)} `];
|
||||||
|
if (have_dn) {
|
||||||
|
s.push(TorrentRendererHelper.formatDL(t));
|
||||||
|
}
|
||||||
|
if (have_up) {
|
||||||
|
s.push(TorrentRendererHelper.formatUL(t));
|
||||||
|
}
|
||||||
|
return s.join(' ');
|
||||||
|
}
|
||||||
|
if (t.isSeeding()) {
|
||||||
|
return `Ratio: ${Formatter.ratioString(
|
||||||
|
t.getUploadRatio()
|
||||||
|
)}, ${TorrentRendererHelper.formatUL(t)}`;
|
||||||
|
}
|
||||||
|
return t.getStateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
render(controller, t, root) {
|
||||||
|
// name
|
||||||
|
let e = root._name_container;
|
||||||
|
e.classList.toggle('paused', t.isStopped());
|
||||||
|
setTextContent(e, t.getName());
|
||||||
|
|
||||||
|
// peer details
|
||||||
|
const has_error = t.getError() !== Torrent._ErrNone;
|
||||||
|
e = root._details_container;
|
||||||
|
e.classList.toggle('error', has_error);
|
||||||
|
setTextContent(e, TorrentRendererCompact.getPeerDetails(t));
|
||||||
|
|
||||||
|
// progressbar
|
||||||
|
TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
|
||||||
|
root._progressbar.classList.add('compact');
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
createRow(torrent) {
|
||||||
|
const progressbar = document.createElement('div');
|
||||||
|
progressbar.classList.add('torrent-progress-bar', 'compact');
|
||||||
|
|
||||||
|
const icon = document.createElement('div');
|
||||||
|
icon.classList.add('icon');
|
||||||
|
icon.dataset.iconMimeType = torrent
|
||||||
|
.getPrimaryMimeType()
|
||||||
|
.split('/', 1)
|
||||||
|
.pop();
|
||||||
|
icon.dataset.iconMultifile = torrent.getFileCount() > 1 ? 'true' : 'false';
|
||||||
|
|
||||||
|
const details = document.createElement('div');
|
||||||
|
details.className = 'torrent-peer-details compact';
|
||||||
|
|
||||||
|
const name = document.createElement('div');
|
||||||
|
name.className = 'torrent-name compact';
|
||||||
|
|
||||||
|
const root = document.createElement('li');
|
||||||
|
root.append(progressbar);
|
||||||
|
root.append(details);
|
||||||
|
root.append(name);
|
||||||
|
root.append(icon);
|
||||||
|
root.className = 'torrent compact';
|
||||||
|
root._progressbar = progressbar;
|
||||||
|
root._details_container = details;
|
||||||
|
root._name_container = name;
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
export class TorrentRow {
|
||||||
|
constructor(view, controller, torrent) {
|
||||||
|
this._view = view;
|
||||||
|
this._torrent = torrent;
|
||||||
|
this._element = view.createRow(torrent);
|
||||||
|
|
||||||
|
const update = () => this.render(controller);
|
||||||
|
this._torrent.addEventListener('dataChanged', update);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
getElement() {
|
||||||
|
return this._element;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(controller) {
|
||||||
|
const tor = this.getTorrent();
|
||||||
|
if (tor) {
|
||||||
|
this._view.render(controller, tor, this.getElement());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelected() {
|
||||||
|
return this.getElement().classList.contains('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
getTorrent() {
|
||||||
|
return this._torrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTorrentId() {
|
||||||
|
return this.getTorrent().getId();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,668 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Formatter } from './formatter.js';
|
||||||
|
import { Prefs } from './prefs.js';
|
||||||
|
import { deepEqual } from './utils.js';
|
||||||
|
|
||||||
|
/// DOMAINS
|
||||||
|
|
||||||
|
// example: "tracker.ubuntu.com" returns "ubuntu.com"
|
||||||
|
function getDomainName(host) {
|
||||||
|
const dot = host.indexOf('.');
|
||||||
|
if (dot !== host.lastIndexOf('.')) {
|
||||||
|
host = host.slice(dot + 1);
|
||||||
|
}
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
// example: "ubuntu.com" returns "Ubuntu"
|
||||||
|
function getReadableDomain(name) {
|
||||||
|
if (name.length > 0) {
|
||||||
|
name = name.charAt(0).toUpperCase() + name.slice(1);
|
||||||
|
}
|
||||||
|
const dot = name.indexOf('.');
|
||||||
|
if (dot !== -1) {
|
||||||
|
name = name.slice(0, dot);
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// key: url string
|
||||||
|
// val: { domain, readable_domain }
|
||||||
|
const announce_to_domain_cache = {};
|
||||||
|
|
||||||
|
function getAnnounceDomain(announce) {
|
||||||
|
if (announce_to_domain_cache[announce]) {
|
||||||
|
return announce_to_domain_cache[announce];
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(announce);
|
||||||
|
const domain = getDomainName(url.host);
|
||||||
|
const name = getReadableDomain(domain);
|
||||||
|
const o = { domain, name, url };
|
||||||
|
announce_to_domain_cache[announce] = o;
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
export class Torrent extends EventTarget {
|
||||||
|
constructor(data) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.fieldObservers = {};
|
||||||
|
this.fields = {};
|
||||||
|
this.refresh(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyOnFieldChange(field, callback) {
|
||||||
|
this.fieldObservers[field] = this.fieldObservers[field] || [];
|
||||||
|
this.fieldObservers[field].push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
setField(o, name, value) {
|
||||||
|
const old_value = o[name];
|
||||||
|
|
||||||
|
if (deepEqual(old_value, value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observers = this.fieldObservers[name];
|
||||||
|
if (o === this.fields && observers && observers.length > 0) {
|
||||||
|
for (const observer of observers) {
|
||||||
|
observer.call(this, value, old_value, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o[name] = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fields.files is an array of unions of RPC's "files" and "fileStats" objects.
|
||||||
|
updateFiles(files) {
|
||||||
|
let changed = false;
|
||||||
|
const myfiles = this.fields.files || [];
|
||||||
|
const keys = ['length', 'name', 'bytesCompleted', 'wanted', 'priority'];
|
||||||
|
|
||||||
|
for (const [index, f] of files.entries()) {
|
||||||
|
const myfile = myfiles[index] || {};
|
||||||
|
for (const key of keys) {
|
||||||
|
if (key in f) {
|
||||||
|
changed |= this.setField(myfile, key, f[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
myfiles[index] = myfile;
|
||||||
|
}
|
||||||
|
this.fields.files = myfiles;
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static collateTrackers(trackers) {
|
||||||
|
return trackers.map((t) => t.announce.toLowerCase()).join('\t');
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshFields(data) {
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
switch (key) {
|
||||||
|
case 'files':
|
||||||
|
case 'fileStats': // merge files and fileStats together
|
||||||
|
changed |= this.updateFiles(value);
|
||||||
|
break;
|
||||||
|
case 'trackerStats': // 'trackerStats' is a superset of 'trackers'...
|
||||||
|
changed |= this.setField(this.fields, 'trackers', value);
|
||||||
|
break;
|
||||||
|
case 'trackers': // ...so only save 'trackers' if we don't have it already
|
||||||
|
if (!(key in this.fields)) {
|
||||||
|
changed |= this.setField(this.fields, key, value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
changed |= this.setField(this.fields, key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh(data) {
|
||||||
|
if (this.refreshFields(data)) {
|
||||||
|
this.dispatchEvent(new Event('dataChanged'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
// simple accessors
|
||||||
|
getComment() {
|
||||||
|
return this.fields.comment;
|
||||||
|
}
|
||||||
|
getCreator() {
|
||||||
|
return this.fields.creator;
|
||||||
|
}
|
||||||
|
getDateAdded() {
|
||||||
|
return this.fields.addedDate;
|
||||||
|
}
|
||||||
|
getDateCreated() {
|
||||||
|
return this.fields.dateCreated;
|
||||||
|
}
|
||||||
|
getDesiredAvailable() {
|
||||||
|
return this.fields.desiredAvailable;
|
||||||
|
}
|
||||||
|
getDownloadDir() {
|
||||||
|
return this.fields.downloadDir;
|
||||||
|
}
|
||||||
|
getDownloadSpeed() {
|
||||||
|
return this.fields.rateDownload;
|
||||||
|
}
|
||||||
|
getDownloadedEver() {
|
||||||
|
return this.fields.downloadedEver;
|
||||||
|
}
|
||||||
|
getError() {
|
||||||
|
return this.fields.error;
|
||||||
|
}
|
||||||
|
getErrorString() {
|
||||||
|
return this.fields.errorString;
|
||||||
|
}
|
||||||
|
getETA() {
|
||||||
|
return this.fields.eta;
|
||||||
|
}
|
||||||
|
getFailedEver() {
|
||||||
|
return this.fields.corruptEver;
|
||||||
|
}
|
||||||
|
getFiles() {
|
||||||
|
return this.fields.files || [];
|
||||||
|
}
|
||||||
|
getFile(index) {
|
||||||
|
return this.fields.files[index];
|
||||||
|
}
|
||||||
|
getFileCount() {
|
||||||
|
return this.fields['file-count'];
|
||||||
|
}
|
||||||
|
getHashString() {
|
||||||
|
return this.fields.hashString;
|
||||||
|
}
|
||||||
|
getHave() {
|
||||||
|
return this.getHaveValid() + this.getHaveUnchecked();
|
||||||
|
}
|
||||||
|
getHaveUnchecked() {
|
||||||
|
return this.fields.haveUnchecked;
|
||||||
|
}
|
||||||
|
getHaveValid() {
|
||||||
|
return this.fields.haveValid;
|
||||||
|
}
|
||||||
|
getId() {
|
||||||
|
return this.fields.id;
|
||||||
|
}
|
||||||
|
getLastActivity() {
|
||||||
|
return this.fields.activityDate;
|
||||||
|
}
|
||||||
|
getLeftUntilDone() {
|
||||||
|
return this.fields.leftUntilDone;
|
||||||
|
}
|
||||||
|
getMetadataPercentComplete() {
|
||||||
|
return this.fields.metadataPercentComplete;
|
||||||
|
}
|
||||||
|
getName() {
|
||||||
|
return this.fields.name || 'Unknown';
|
||||||
|
}
|
||||||
|
getPeers() {
|
||||||
|
return this.fields.peers || [];
|
||||||
|
}
|
||||||
|
getPeersConnected() {
|
||||||
|
return this.fields.peersConnected;
|
||||||
|
}
|
||||||
|
getPeersGettingFromUs() {
|
||||||
|
return this.fields.peersGettingFromUs;
|
||||||
|
}
|
||||||
|
getPeersSendingToUs() {
|
||||||
|
return this.fields.peersSendingToUs;
|
||||||
|
}
|
||||||
|
getPieceCount() {
|
||||||
|
return this.fields.pieceCount;
|
||||||
|
}
|
||||||
|
getPieceSize() {
|
||||||
|
return this.fields.pieceSize;
|
||||||
|
}
|
||||||
|
getPrimaryMimeType() {
|
||||||
|
return this.fields['primary-mime-type'];
|
||||||
|
}
|
||||||
|
getPrivateFlag() {
|
||||||
|
return this.fields.isPrivate;
|
||||||
|
}
|
||||||
|
getQueuePosition() {
|
||||||
|
return this.fields.queuePosition;
|
||||||
|
}
|
||||||
|
getRecheckProgress() {
|
||||||
|
return this.fields.recheckProgress;
|
||||||
|
}
|
||||||
|
getSeedRatioLimit() {
|
||||||
|
return this.fields.seedRatioLimit;
|
||||||
|
}
|
||||||
|
getSeedRatioMode() {
|
||||||
|
return this.fields.seedRatioMode;
|
||||||
|
}
|
||||||
|
getSizeWhenDone() {
|
||||||
|
return this.fields.sizeWhenDone;
|
||||||
|
}
|
||||||
|
getStartDate() {
|
||||||
|
return this.fields.startDate;
|
||||||
|
}
|
||||||
|
getStatus() {
|
||||||
|
return this.fields.status;
|
||||||
|
}
|
||||||
|
getTotalSize() {
|
||||||
|
return this.fields.totalSize;
|
||||||
|
}
|
||||||
|
getTrackers() {
|
||||||
|
const trackers = this.fields.trackers || [];
|
||||||
|
for (const tracker of trackers) {
|
||||||
|
if (tracker.announce && !tracker.domain) {
|
||||||
|
Object.assign(tracker, getAnnounceDomain(tracker.announce));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.fields.trackers;
|
||||||
|
}
|
||||||
|
getUploadSpeed() {
|
||||||
|
return this.fields.rateUpload;
|
||||||
|
}
|
||||||
|
getUploadRatio() {
|
||||||
|
return this.fields.uploadRatio;
|
||||||
|
}
|
||||||
|
getUploadedEver() {
|
||||||
|
return this.fields.uploadedEver;
|
||||||
|
}
|
||||||
|
getWebseedsSendingToUs() {
|
||||||
|
return this.fields.webseedsSendingToUs;
|
||||||
|
}
|
||||||
|
isFinished() {
|
||||||
|
return this.fields.isFinished;
|
||||||
|
}
|
||||||
|
|
||||||
|
// derived accessors
|
||||||
|
hasExtraInfo() {
|
||||||
|
return 'hashString' in this.fields;
|
||||||
|
}
|
||||||
|
isSeeding() {
|
||||||
|
return this.getStatus() === Torrent._StatusSeed;
|
||||||
|
}
|
||||||
|
isStopped() {
|
||||||
|
return this.getStatus() === Torrent._StatusStopped;
|
||||||
|
}
|
||||||
|
isChecking() {
|
||||||
|
return this.getStatus() === Torrent._StatusCheck;
|
||||||
|
}
|
||||||
|
isDownloading() {
|
||||||
|
return this.getStatus() === Torrent._StatusDownload;
|
||||||
|
}
|
||||||
|
isQueued() {
|
||||||
|
return (
|
||||||
|
this.getStatus() === Torrent._StatusDownloadWait ||
|
||||||
|
this.getStatus() === Torrent._StatusSeedWait
|
||||||
|
);
|
||||||
|
}
|
||||||
|
isDone() {
|
||||||
|
return this.getLeftUntilDone() < 1;
|
||||||
|
}
|
||||||
|
needsMetaData() {
|
||||||
|
return this.getMetadataPercentComplete() < 1;
|
||||||
|
}
|
||||||
|
getActivity() {
|
||||||
|
return this.getDownloadSpeed() + this.getUploadSpeed();
|
||||||
|
}
|
||||||
|
getPercentDoneStr() {
|
||||||
|
return Formatter.percentString(100 * this.getPercentDone());
|
||||||
|
}
|
||||||
|
getPercentDone() {
|
||||||
|
return this.fields.percentDone;
|
||||||
|
}
|
||||||
|
getStateString() {
|
||||||
|
switch (this.getStatus()) {
|
||||||
|
case Torrent._StatusStopped:
|
||||||
|
return this.isFinished() ? 'Seeding complete' : 'Paused';
|
||||||
|
case Torrent._StatusCheckWait:
|
||||||
|
return 'Queued for verification';
|
||||||
|
case Torrent._StatusCheck:
|
||||||
|
return 'Verifying local data';
|
||||||
|
case Torrent._StatusDownloadWait:
|
||||||
|
return 'Queued for download';
|
||||||
|
case Torrent._StatusDownload:
|
||||||
|
return 'Downloading';
|
||||||
|
case Torrent._StatusSeedWait:
|
||||||
|
return 'Queued for seeding';
|
||||||
|
case Torrent._StatusSeed:
|
||||||
|
return 'Seeding';
|
||||||
|
case null:
|
||||||
|
return 'Unknown';
|
||||||
|
default:
|
||||||
|
return 'Error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seedRatioLimit(controller) {
|
||||||
|
switch (this.getSeedRatioMode()) {
|
||||||
|
case Torrent._RatioUseGlobal:
|
||||||
|
return controller.seedRatioLimit();
|
||||||
|
case Torrent._RatioUseLocal:
|
||||||
|
return this.getSeedRatioLimit();
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getErrorMessage() {
|
||||||
|
const string = this.getErrorString();
|
||||||
|
switch (this.getError()) {
|
||||||
|
case Torrent._ErrTrackerWarning:
|
||||||
|
return `Tracker returned a warning: ${string}`;
|
||||||
|
case Torrent._ErrTrackerError:
|
||||||
|
return `Tracker returned an error: ${string}`;
|
||||||
|
case Torrent._ErrLocalError:
|
||||||
|
return `Error: ${string}`;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getCollatedName() {
|
||||||
|
const f = this.fields;
|
||||||
|
if (!f.collatedName && f.name) {
|
||||||
|
f.collatedName = f.name.toLowerCase();
|
||||||
|
}
|
||||||
|
return f.collatedName || '';
|
||||||
|
}
|
||||||
|
getCollatedTrackers() {
|
||||||
|
const f = this.fields;
|
||||||
|
if (!f.collatedTrackers && f.trackers) {
|
||||||
|
f.collatedTrackers = Torrent.collateTrackers(f.trackers);
|
||||||
|
}
|
||||||
|
return f.collatedTrackers || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/****
|
||||||
|
*****
|
||||||
|
****/
|
||||||
|
|
||||||
|
testState(state) {
|
||||||
|
const s = this.getStatus();
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case Prefs.FilterActive:
|
||||||
|
return (
|
||||||
|
this.getPeersGettingFromUs() > 0 ||
|
||||||
|
this.getPeersSendingToUs() > 0 ||
|
||||||
|
this.getWebseedsSendingToUs() > 0 ||
|
||||||
|
this.isChecking()
|
||||||
|
);
|
||||||
|
case Prefs.FilterSeeding:
|
||||||
|
return s === Torrent._StatusSeed || s === Torrent._StatusSeedWait;
|
||||||
|
case Prefs.FilterDownloading:
|
||||||
|
return (
|
||||||
|
s === Torrent._StatusDownload || s === Torrent._StatusDownloadWait
|
||||||
|
);
|
||||||
|
case Prefs.FilterPaused:
|
||||||
|
return this.isStopped();
|
||||||
|
case Prefs.FilterFinished:
|
||||||
|
return this.isFinished();
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param filter one of Prefs.Filter*
|
||||||
|
* @param search substring to look for, or null
|
||||||
|
* @return true if it passes the test, false if it fails
|
||||||
|
*/
|
||||||
|
test(state, search, tracker) {
|
||||||
|
// flter by state...
|
||||||
|
let pass = this.testState(state);
|
||||||
|
|
||||||
|
// maybe filter by text...
|
||||||
|
if (pass && search && search.length > 0) {
|
||||||
|
pass = this.getCollatedName().includes(search.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybe filter by tracker...
|
||||||
|
if (pass && tracker && tracker.length > 0) {
|
||||||
|
pass = this.getCollatedTrackers().includes(tracker);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
static compareById(ta, tb) {
|
||||||
|
return ta.getId() - tb.getId();
|
||||||
|
}
|
||||||
|
static compareByName(ta, tb) {
|
||||||
|
return (
|
||||||
|
ta.getCollatedName().localeCompare(tb.getCollatedName()) ||
|
||||||
|
Torrent.compareById(ta, tb)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
static compareByQueue(ta, tb) {
|
||||||
|
return ta.getQueuePosition() - tb.getQueuePosition();
|
||||||
|
}
|
||||||
|
static compareByAge(ta, tb) {
|
||||||
|
const a = ta.getDateAdded();
|
||||||
|
const b = tb.getDateAdded();
|
||||||
|
|
||||||
|
return b - a || Torrent.compareByQueue(ta, tb);
|
||||||
|
}
|
||||||
|
static compareByState(ta, tb) {
|
||||||
|
const a = ta.getStatus();
|
||||||
|
const b = tb.getStatus();
|
||||||
|
|
||||||
|
return b - a || Torrent.compareByQueue(ta, tb);
|
||||||
|
}
|
||||||
|
static compareByActivity(ta, tb) {
|
||||||
|
const a = ta.getActivity();
|
||||||
|
const b = tb.getActivity();
|
||||||
|
|
||||||
|
return b - a || Torrent.compareByState(ta, tb);
|
||||||
|
}
|
||||||
|
static compareByRatio(ta, tb) {
|
||||||
|
const a = ta.getUploadRatio();
|
||||||
|
const b = tb.getUploadRatio();
|
||||||
|
|
||||||
|
if (a < b) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (a > b) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return Torrent.compareByState(ta, tb);
|
||||||
|
}
|
||||||
|
static compareByProgress(ta, tb) {
|
||||||
|
const a = ta.getPercentDone();
|
||||||
|
const b = tb.getPercentDone();
|
||||||
|
|
||||||
|
return a - b || Torrent.compareByRatio(ta, tb);
|
||||||
|
}
|
||||||
|
static compareBySize(ta, tb) {
|
||||||
|
const a = ta.getTotalSize();
|
||||||
|
const b = tb.getTotalSize();
|
||||||
|
|
||||||
|
return a - b || Torrent.compareByName(ta, tb);
|
||||||
|
}
|
||||||
|
|
||||||
|
static compareTorrents(a, b, sortMode, sortDirection) {
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
switch (sortMode) {
|
||||||
|
case Prefs.SortByActivity:
|
||||||
|
index = Torrent.compareByActivity(a, b);
|
||||||
|
break;
|
||||||
|
case Prefs.SortByAge:
|
||||||
|
index = Torrent.compareByAge(a, b);
|
||||||
|
break;
|
||||||
|
case Prefs.SortByQueue:
|
||||||
|
index = Torrent.compareByQueue(a, b);
|
||||||
|
break;
|
||||||
|
case Prefs.SortByProgress:
|
||||||
|
index = Torrent.compareByProgress(a, b);
|
||||||
|
break;
|
||||||
|
case Prefs.SortBySize:
|
||||||
|
index = Torrent.compareBySize(a, b);
|
||||||
|
break;
|
||||||
|
case Prefs.SortByState:
|
||||||
|
index = Torrent.compareByState(a, b);
|
||||||
|
break;
|
||||||
|
case Prefs.SortByRatio:
|
||||||
|
index = Torrent.compareByRatio(a, b);
|
||||||
|
break;
|
||||||
|
case Prefs.SortByName:
|
||||||
|
index = Torrent.compareByName(a, b);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(`Unrecognized sort mode: ${sortMode}`);
|
||||||
|
index = Torrent.compareByName(a, b);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDirection === Prefs.SortDescending) {
|
||||||
|
index = -index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param torrents an array of Torrent objects
|
||||||
|
* @param sortMode one of Prefs.SortBy*
|
||||||
|
* @param sortDirection Prefs.SortAscending or Prefs.SortDescending
|
||||||
|
*/
|
||||||
|
static sortTorrents(torrents, sortMode, sortDirection) {
|
||||||
|
switch (sortMode) {
|
||||||
|
case Prefs.SortByActivity:
|
||||||
|
torrents.sort(this.compareByActivity);
|
||||||
|
break;
|
||||||
|
case Prefs.SortByAge:
|
||||||
|
torrents.sort(this.compareByAge);
|
||||||
|
break;
|
||||||
|
case Prefs.SortByName:
|
||||||
|
torrents.sort(this.compareByName);
|
||||||
|
break;
|
||||||
|
case Prefs.SortByProgress:
|
||||||
|
torrents.sort(this.compareByProgress);
|
||||||
|
break;
|
||||||
|
case Prefs.SortByQueue:
|
||||||
|
torrents.sort(this.compareByQueue);
|
||||||
|
break;
|
||||||
|
case Prefs.SortByRatio:
|
||||||
|
torrents.sort(this.compareByRatio);
|
||||||
|
break;
|
||||||
|
case Prefs.SortBySize:
|
||||||
|
torrents.sort(this.compareBySize);
|
||||||
|
break;
|
||||||
|
case Prefs.SortByState:
|
||||||
|
torrents.sort(this.compareByState);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(`Unrecognized sort mode: ${sortMode}`);
|
||||||
|
torrents.sort(this.compareByName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDirection === Prefs.SortDescending) {
|
||||||
|
torrents.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
return torrents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Torrent.fields.status
|
||||||
|
Torrent._StatusStopped = 0;
|
||||||
|
Torrent._StatusCheckWait = 1;
|
||||||
|
Torrent._StatusCheck = 2;
|
||||||
|
Torrent._StatusDownloadWait = 3;
|
||||||
|
Torrent._StatusDownload = 4;
|
||||||
|
Torrent._StatusSeedWait = 5;
|
||||||
|
Torrent._StatusSeed = 6;
|
||||||
|
|
||||||
|
// Torrent.fields.seedRatioMode
|
||||||
|
Torrent._RatioUseGlobal = 0;
|
||||||
|
Torrent._RatioUseLocal = 1;
|
||||||
|
Torrent._RatioUnlimited = 2;
|
||||||
|
|
||||||
|
// Torrent.fields.error
|
||||||
|
Torrent._ErrNone = 0;
|
||||||
|
Torrent._ErrTrackerWarning = 1;
|
||||||
|
Torrent._ErrTrackerError = 2;
|
||||||
|
Torrent._ErrLocalError = 3;
|
||||||
|
|
||||||
|
// TrackerStats' announceState
|
||||||
|
Torrent._TrackerInactive = 0;
|
||||||
|
Torrent._TrackerWaiting = 1;
|
||||||
|
Torrent._TrackerQueued = 2;
|
||||||
|
Torrent._TrackerActive = 3;
|
||||||
|
|
||||||
|
Torrent.Fields = {};
|
||||||
|
|
||||||
|
// commonly used fields which only need to be loaded once,
|
||||||
|
// either on startup or when a magnet finishes downloading its metadata
|
||||||
|
// finishes downloading its metadata
|
||||||
|
Torrent.Fields.Metadata = [
|
||||||
|
'addedDate',
|
||||||
|
'file-count',
|
||||||
|
'name',
|
||||||
|
'primary-mime-type',
|
||||||
|
'totalSize',
|
||||||
|
];
|
||||||
|
|
||||||
|
// commonly used fields which need to be periodically refreshed
|
||||||
|
Torrent.Fields.Stats = [
|
||||||
|
'error',
|
||||||
|
'errorString',
|
||||||
|
'eta',
|
||||||
|
'isFinished',
|
||||||
|
'isStalled',
|
||||||
|
'leftUntilDone',
|
||||||
|
'metadataPercentComplete',
|
||||||
|
'peersConnected',
|
||||||
|
'peersGettingFromUs',
|
||||||
|
'peersSendingToUs',
|
||||||
|
'percentDone',
|
||||||
|
'queuePosition',
|
||||||
|
'rateDownload',
|
||||||
|
'rateUpload',
|
||||||
|
'recheckProgress',
|
||||||
|
'seedRatioMode',
|
||||||
|
'seedRatioLimit',
|
||||||
|
'sizeWhenDone',
|
||||||
|
'status',
|
||||||
|
'trackers',
|
||||||
|
'downloadDir',
|
||||||
|
'uploadedEver',
|
||||||
|
'uploadRatio',
|
||||||
|
'webseedsSendingToUs',
|
||||||
|
];
|
||||||
|
|
||||||
|
// fields used by the inspector which only need to be loaded once
|
||||||
|
Torrent.Fields.InfoExtra = [
|
||||||
|
'comment',
|
||||||
|
'creator',
|
||||||
|
'dateCreated',
|
||||||
|
'files',
|
||||||
|
'hashString',
|
||||||
|
'isPrivate',
|
||||||
|
'pieceCount',
|
||||||
|
'pieceSize',
|
||||||
|
];
|
||||||
|
|
||||||
|
// fields used in the inspector which need to be periodically refreshed
|
||||||
|
Torrent.Fields.StatsExtra = [
|
||||||
|
'activityDate',
|
||||||
|
'corruptEver',
|
||||||
|
'desiredAvailable',
|
||||||
|
'downloadedEver',
|
||||||
|
'fileStats',
|
||||||
|
'haveUnchecked',
|
||||||
|
'haveValid',
|
||||||
|
'peers',
|
||||||
|
'startDate',
|
||||||
|
'trackerStats',
|
||||||
|
];
|
|
@ -0,0 +1,278 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2020 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import isEqual from 'lodash.isequal';
|
||||||
|
|
||||||
|
export class Utils {
|
||||||
|
/**
|
||||||
|
* Checks to see if the content actually changed before poking the DOM.
|
||||||
|
*/
|
||||||
|
static setInnerHTML(e, html) {
|
||||||
|
if (!e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* innerHTML is listed as a string, but the browser seems to change it.
|
||||||
|
* For example, "∞" gets changed to "∞" somewhere down the line.
|
||||||
|
* So, let's use an arbitrary different field to test our state... */
|
||||||
|
if (e.currentHTML !== html) {
|
||||||
|
e.currentHTML = html;
|
||||||
|
e.innerHTML = html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Given a numerator and denominator, return a ratio string */
|
||||||
|
static ratio(numerator, denominator) {
|
||||||
|
let result = Math.floor((100 * numerator) / denominator) / 100;
|
||||||
|
|
||||||
|
// check for special cases
|
||||||
|
if (
|
||||||
|
result === Number.POSITIVE_INFINITY ||
|
||||||
|
result === Number.NEGATIVE_INFINITY
|
||||||
|
) {
|
||||||
|
result = -2;
|
||||||
|
} else if (Number.isNaN(result)) {
|
||||||
|
result = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTabsContainer(id, tabs, callback) {
|
||||||
|
const root = document.createElement('div');
|
||||||
|
root.id = id;
|
||||||
|
root.classList.add('tabs-container');
|
||||||
|
|
||||||
|
const buttons = document.createElement('div');
|
||||||
|
buttons.classList.add('tabs-buttons');
|
||||||
|
root.append(buttons);
|
||||||
|
|
||||||
|
const pages = document.createElement('div');
|
||||||
|
pages.classList.add('tabs-pages');
|
||||||
|
root.append(pages);
|
||||||
|
|
||||||
|
const button_array = [];
|
||||||
|
for (const [button_id, page] of tabs) {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.id = button_id;
|
||||||
|
button.classList.add('tabs-button');
|
||||||
|
button.setAttribute('type', 'button');
|
||||||
|
buttons.append(button);
|
||||||
|
button_array.push(button);
|
||||||
|
|
||||||
|
page.classList.add('hidden', 'tabs-page');
|
||||||
|
pages.append(page);
|
||||||
|
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
for (const element of buttons.children) {
|
||||||
|
element.classList.toggle('selected', element === button);
|
||||||
|
}
|
||||||
|
for (const element of pages.children) {
|
||||||
|
element.classList.toggle('hidden', element !== page);
|
||||||
|
}
|
||||||
|
if (callback) {
|
||||||
|
callback(page);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
button_array[0].classList.add('selected');
|
||||||
|
pages.children[0].classList.remove('hidden');
|
||||||
|
|
||||||
|
return {
|
||||||
|
buttons: button_array,
|
||||||
|
root,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDialogContainer(id) {
|
||||||
|
const root = document.createElement('dialog');
|
||||||
|
root.classList.add('dialog-container', 'popup', id);
|
||||||
|
root.open = true;
|
||||||
|
root.setAttribute('role', 'dialog');
|
||||||
|
|
||||||
|
const win = document.createElement('div');
|
||||||
|
win.classList.add('dialog-window');
|
||||||
|
root.append(win);
|
||||||
|
|
||||||
|
const logo = document.createElement('div');
|
||||||
|
logo.classList.add('dialog-logo');
|
||||||
|
win.append(logo);
|
||||||
|
|
||||||
|
const heading = document.createElement('div');
|
||||||
|
heading.classList.add('dialog-heading');
|
||||||
|
win.append(heading);
|
||||||
|
|
||||||
|
const message = document.createElement('div');
|
||||||
|
message.classList.add('dialog-message');
|
||||||
|
win.append(message);
|
||||||
|
|
||||||
|
const workarea = document.createElement('div');
|
||||||
|
workarea.classList.add('dialog-workarea');
|
||||||
|
win.append(workarea);
|
||||||
|
|
||||||
|
const buttons = document.createElement('div');
|
||||||
|
buttons.classList.add('dialog-buttons');
|
||||||
|
win.append(buttons);
|
||||||
|
|
||||||
|
const bbegin = document.createElement('span');
|
||||||
|
bbegin.classList.add('dialog-buttons-begin');
|
||||||
|
buttons.append(bbegin);
|
||||||
|
|
||||||
|
const dismiss = document.createElement('button');
|
||||||
|
dismiss.classList.add('dialog-dismiss-button');
|
||||||
|
dismiss.textContent = 'Cancel';
|
||||||
|
buttons.append(dismiss);
|
||||||
|
|
||||||
|
const confirm = document.createElement('button');
|
||||||
|
confirm.textContent = 'OK';
|
||||||
|
buttons.append(confirm);
|
||||||
|
|
||||||
|
const bend = document.createElement('span');
|
||||||
|
bend.classList.add('dialog-buttons-end');
|
||||||
|
buttons.append(bend);
|
||||||
|
|
||||||
|
return {
|
||||||
|
confirm,
|
||||||
|
dismiss,
|
||||||
|
heading,
|
||||||
|
message,
|
||||||
|
root,
|
||||||
|
workarea,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeUUID() {
|
||||||
|
// source: https://stackoverflow.com/a/2117523/6568470
|
||||||
|
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
|
||||||
|
(
|
||||||
|
c ^
|
||||||
|
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
|
||||||
|
).toString(16)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSection(title) {
|
||||||
|
const root = document.createElement('fieldset');
|
||||||
|
root.classList.add('section');
|
||||||
|
|
||||||
|
const legend = document.createElement('legend');
|
||||||
|
legend.classList.add('title');
|
||||||
|
legend.textContent = title;
|
||||||
|
root.append(legend);
|
||||||
|
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.classList.add('content');
|
||||||
|
root.append(content);
|
||||||
|
|
||||||
|
return { content, root };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createInfoSection(title, labels) {
|
||||||
|
const children = [];
|
||||||
|
const { root, content } = createSection(title);
|
||||||
|
|
||||||
|
for (const label_text of labels) {
|
||||||
|
const label_element = document.createElement('label');
|
||||||
|
label_element.textContent = label_text;
|
||||||
|
content.append(label_element);
|
||||||
|
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.id = makeUUID();
|
||||||
|
content.append(item);
|
||||||
|
label_element.setAttribute('for', item.id);
|
||||||
|
children.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { children, root };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debounce(callback, wait = 100) {
|
||||||
|
let timeout = null;
|
||||||
|
return (...arguments_) => {
|
||||||
|
if (!timeout) {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
timeout = null;
|
||||||
|
callback(arguments_);
|
||||||
|
}, wait);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deepEqual(a, b) {
|
||||||
|
return isEqual(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOrDeleteAttribute(element, attribute, b) {
|
||||||
|
if (b) {
|
||||||
|
element.setAttribute(attribute, true);
|
||||||
|
} else {
|
||||||
|
element.removeAttribute(attribute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function setEnabled(element, b) {
|
||||||
|
setOrDeleteAttribute(element, 'disabled', !b);
|
||||||
|
}
|
||||||
|
export function setChecked(element, b) {
|
||||||
|
setOrDeleteAttribute(element, 'checked', b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBestMenuPos(r, bounds) {
|
||||||
|
let { x, y } = r;
|
||||||
|
const { width, height } = r;
|
||||||
|
|
||||||
|
if (x > bounds.x + bounds.width - width && x - width >= bounds.x) {
|
||||||
|
x -= width;
|
||||||
|
} else {
|
||||||
|
x = Math.min(x, bounds.x + bounds.width - width);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y > bounds.y + bounds.height - height && y - height >= bounds.y) {
|
||||||
|
y -= height;
|
||||||
|
} else {
|
||||||
|
y = Math.min(y, bounds.y + bounds.height - height);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DOMRect(x, y, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function movePopup(popup, x, y, boundingElement) {
|
||||||
|
const initial_pos = new DOMRect(x, y, popup.clientWidth, popup.clientHeight);
|
||||||
|
const clamped_pos = getBestMenuPos(
|
||||||
|
initial_pos,
|
||||||
|
boundingElement.getBoundingClientRect()
|
||||||
|
);
|
||||||
|
popup.style.left = `${clamped_pos.left}px`;
|
||||||
|
popup.style.top = `${clamped_pos.top}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OutsideClickListener extends EventTarget {
|
||||||
|
constructor(element) {
|
||||||
|
super();
|
||||||
|
this.listener = (event_) => {
|
||||||
|
if (!element.contains(event_.target)) {
|
||||||
|
this.dispatchEvent(new MouseEvent(event_.type, event_));
|
||||||
|
event_.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Object.seal(this);
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
setTimeout(() => document.addEventListener('click', this.listener), 0);
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
document.removeEventListener('click', this.listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTextContent(e, text) {
|
||||||
|
if (e.textContent !== text) {
|
||||||
|
e.textContent = text;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
|
||||||
|
# GOT BETTER ICONS? PULL REQUESTS WELCOMED
|
||||||
|
|
||||||
|
## Material Icons
|
||||||
|
|
||||||
|
https://github.com/google/material-design-icons/
|
||||||
|
https://material.io/resources/icons/
|
||||||
|
|
||||||
|
* analytics.svg
|
||||||
|
* horizontal-rule.svg
|
||||||
|
* pause-circle-active.svg (pause-circle-filled.svg)
|
||||||
|
* pause-circle-idle.svg (pause-circle-filled.svg)
|
||||||
|
* play-circle-active.svg (play-circle-filled.svg)
|
||||||
|
* play-circle-idle.svg (play-circle-filled.svg)
|
||||||
|
* router.svg
|
||||||
|
|
||||||
|
## Bootstrap Icons
|
||||||
|
|
||||||
|
https://github.com/twbs/icons
|
||||||
|
https://icons.getbootstrap.com/icons/
|
||||||
|
license: MIT
|
||||||
|
|
||||||
|
* chevron-down.svg
|
||||||
|
* chevron-up.svg
|
||||||
|
* files.svg
|
||||||
|
* gear-fill.svg
|
||||||
|
* lock-fill.svg
|
||||||
|
* search.svg
|
||||||
|
* three-dots-vertical.svg
|
||||||
|
|
||||||
|
## Adwaita Icons
|
||||||
|
|
||||||
|
https://gitlab.gnome.org/GNOME/adwaita-icon-theme
|
||||||
|
license: CC-BY-SA 3.0
|
||||||
|
|
||||||
|
* audio-x-generic.png
|
||||||
|
* folder.png
|
||||||
|
* font-x-generic.png
|
||||||
|
* image-x-generic.png
|
||||||
|
* package-x-generic.png
|
||||||
|
* text-x-generic.png
|
||||||
|
* video-x-generic.png
|
||||||
|
|
||||||
|
|
||||||
|
## SVG Repo
|
||||||
|
|
||||||
|
https://www.svgrepo.com/
|
||||||
|
license: CC0
|
||||||
|
|
||||||
|
* checkered-flag.svg
|
||||||
|
* cloud-networking.svg
|
||||||
|
* globa-server.svg
|
||||||
|
* team.svg
|
||||||
|
* top-speed.svg (license: MIT)
|
||||||
|
* up-and-down-arrows.svg
|
||||||
|
|
||||||
|
|
||||||
|
## Custom Icons
|
||||||
|
|
||||||
|
license: MIT
|
||||||
|
|
||||||
|
* blue-turtle.png
|
||||||
|
* logo.png
|
||||||
|
* toolbar-close.png
|
||||||
|
* toolbar-folder.png
|
||||||
|
* toolbar-info.png
|
||||||
|
* toolbar-pause.png
|
||||||
|
* toolbar-start.png
|
||||||
|
* turtle.png
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-5h2v5zm4 0h-2v-3h2v3zm0-5h-2v-2h2v2zm4 5h-2V7h2v10z"/></svg>
|
After Width: | Height: | Size: 274 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 227 B |
|
@ -0,0 +1,62 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="453.405px" height="453.405px" viewBox="0 0 453.405 453.405" style="enable-background:new 0 0 453.405 453.405;"
|
||||||
|
xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M382.08,60.394c-26.324-4.534-53.444-0.845-79.764,1.751c-26.223,2.587-53.604,5.753-79.585-0.397
|
||||||
|
c-30.592-7.241-49.945-27.294-64.216-54.464c-3.935,10.646-7.869,21.291-11.803,31.938
|
||||||
|
c-25.74,69.646-51.479,139.292-77.218,208.938L0,436.203l26.838,9.919l62.541-169.227c11.607,12.383,25.937,21.375,44.333,25.729
|
||||||
|
c25.979,6.146,53.363,2.986,79.584,0.398c26.318-2.601,53.441-6.287,79.765-1.752c33.826,5.826,55.682,26.086,71.323,55.871
|
||||||
|
c29.677-80.291,59.348-160.583,89.021-240.876C437.761,86.479,415.911,66.222,382.08,60.394z M385.379,203.349
|
||||||
|
c-13.234-11.169-27.441-18.638-44.57-21.931c-5.715,15.458-11.428,30.916-17.141,46.374c17.131,3.295,31.335,10.764,44.572,21.932
|
||||||
|
c-5.239,14.176-10.479,28.353-15.717,42.526c-13.234-11.168-27.443-18.642-44.573-21.93c5.239-14.177,10.479-28.353,15.718-42.528
|
||||||
|
c-17.442-2.813-34.473-2.797-52.072-1.72c-5.238,14.176-10.479,28.353-15.717,42.528c-18.21,1.471-36.358,3.56-54.567,5.028
|
||||||
|
c5.238-14.178,10.478-28.353,15.716-42.526c-17.599,1.078-34.631,1.096-52.073-1.719c-5.239,14.176-10.478,28.352-15.717,42.526
|
||||||
|
c-17.128-3.29-31.341-10.763-44.572-21.933c5.238-14.174,10.478-28.351,15.716-42.525c13.236,11.17,27.442,18.64,44.573,21.932
|
||||||
|
c5.712-15.458,11.427-30.918,17.139-46.376c-17.13-3.285-31.338-10.766-44.573-21.93c5.714-15.46,11.427-30.92,17.14-46.378
|
||||||
|
c13.236,11.173,27.442,18.635,44.572,21.933c5.239-14.176,10.478-28.351,15.717-42.525c17.442,2.813,34.476,2.797,52.073,1.717
|
||||||
|
c-5.238,14.175-10.478,28.351-15.717,42.526c18.209-1.471,36.357-3.558,54.567-5.028c5.238-14.175,10.479-28.351,15.717-42.527
|
||||||
|
c17.601-1.078,34.629-1.095,52.072,1.719c-5.239,14.176-10.478,28.351-15.717,42.528c17.131,3.294,31.335,10.761,44.573,21.93
|
||||||
|
C396.806,172.431,391.095,187.891,385.379,203.349z"/>
|
||||||
|
<path d="M234.167,184.726c-5.713,15.459-11.426,30.917-17.14,46.376c18.21-1.472,36.359-3.56,54.568-5.03
|
||||||
|
c5.713-15.457,11.426-30.916,17.139-46.374C270.523,181.169,252.376,183.257,234.167,184.726z"/>
|
||||||
|
<path d="M234.167,184.726c5.714-15.458,11.427-30.918,17.14-46.375c-17.604,1.075-34.629,1.093-52.075-1.718
|
||||||
|
c-5.713,15.458-11.426,30.917-17.139,46.375C199.536,185.824,216.566,185.807,234.167,184.726z"/>
|
||||||
|
<path d="M305.873,133.323c-5.713,15.458-11.426,30.916-17.139,46.375c17.601-1.075,34.629-1.093,52.073,1.72
|
||||||
|
c5.712-15.458,11.426-30.917,17.138-46.375C340.503,132.229,323.474,132.243,305.873,133.323z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-chevron-down" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 292 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-chevron-up" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708l6-6z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 273 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-diagram-3-fill" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M6 3.5A1.5 1.5 0 0 1 7.5 2h1A1.5 1.5 0 0 1 10 3.5v1A1.5 1.5 0 0 1 8.5 6v1H14a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0V8h-5v.5a.5.5 0 0 1-1 0V8h-5v.5a.5.5 0 0 1-1 0v-1A.5.5 0 0 1 2 7h5.5V6A1.5 1.5 0 0 1 6 4.5v-1zm-6 8A1.5 1.5 0 0 1 1.5 10h1A1.5 1.5 0 0 1 4 11.5v1A1.5 1.5 0 0 1 2.5 14h-1A1.5 1.5 0 0 1 0 12.5v-1zm6 0A1.5 1.5 0 0 1 7.5 10h1a1.5 1.5 0 0 1 1.5 1.5v1A1.5 1.5 0 0 1 8.5 14h-1A1.5 1.5 0 0 1 6 12.5v-1zm6 0a1.5 1.5 0 0 1 1.5-1.5h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5v-1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 683 B |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-files" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M4 2h7a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm0 1a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4z"/>
|
||||||
|
<path d="M6 0h7a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2v-1a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H6a1 1 0 0 0-1 1H4a2 2 0 0 1 2-2z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 425 B |
After Width: | Height: | Size: 344 B |
After Width: | Height: | Size: 796 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-gear-fill" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 0 0-5.86 2.929 2.929 0 0 0 0 5.858z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 841 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" fill-rule="evenodd" height="24" width="24"/><rect fill-rule="evenodd" height="2" width="16" x="4" y="11"/></g></svg>
|
After Width: | Height: | Size: 254 B |
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-lock-fill" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.5 9a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-7a2 2 0 0 1-2-2V9z"/>
|
||||||
|
<path fill-rule="evenodd" d="M4.5 4a3.5 3.5 0 1 1 7 0v3h-1V4a2.5 2.5 0 0 0-5 0v3h-1V4z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 315 B |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 610 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="#800" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z"/></svg>
|
After Width: | Height: | Size: 246 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="#999" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z"/></svg>
|
After Width: | Height: | Size: 246 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="#080" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg>
|
After Width: | Height: | Size: 241 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="#999" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg>
|
After Width: | Height: | Size: 241 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M20.2 5.9l.8-.8C19.6 3.7 17.8 3 16 3s-3.6.7-5 2.1l.8.8C13 4.8 14.5 4.2 16 4.2s3 .6 4.2 1.7zm-.9.8c-.9-.9-2.1-1.4-3.3-1.4s-2.4.5-3.3 1.4l.8.8c.7-.7 1.6-1 2.5-1 .9 0 1.8.3 2.5 1l.8-.8zM19 13h-2V9h-2v4H5c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-4c0-1.1-.9-2-2-2zM8 18H6v-2h2v2zm3.5 0h-2v-2h2v2zm3.5 0h-2v-2h2v2z"/></svg>
|
After Width: | Height: | Size: 456 B |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/>
|
||||||
|
<path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 389 B |
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="122.699px" height="122.699px" viewBox="0 0 122.699 122.699" style="enable-background:new 0 0 122.699 122.699;"
|
||||||
|
xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<circle cx="19.5" cy="12.2" r="12.1"/>
|
||||||
|
<path d="M6,66.699h1.2v24c0,3.301,2.7,6,6,6h12.6c3.3,0,6-2.699,6-6V89.3c-1.1-2.101-1.8-4.5-1.8-7v-31.4c0-6.1,3.7-11.4,9-13.7
|
||||||
|
v-2.4c0-3.3-2.7-6-6-6H6c-3.3,0-6,2.7-6,6v25.9C0,64,2.6,66.699,6,66.699z"/>
|
||||||
|
<circle cx="103.3" cy="12.2" r="12.1"/>
|
||||||
|
<path d="M83.699,34.7v2.4c5.301,2.3,9,7.6,9,13.7v31.3c0,2.5-0.6,4.9-1.799,7v1.4c0,3.3,2.699,6,6,6h12.6c3.3,0,6-2.7,6-6v-24
|
||||||
|
h1.199c3.301,0,6-2.7,6-6V34.7c0-3.3-2.699-6-6-6h-27C86.4,28.7,83.699,31.399,83.699,34.7z"/>
|
||||||
|
<path d="M39.1,50.899L39.1,50.899v9.8v21.6c0,3.3,2.7,6,6,6h2.3v28.3c0,3.3,2.7,6,6,6h16.1c3.3,0,6-2.7,6-6v-28.4h2.3
|
||||||
|
c3.3,0,6-2.699,6-6V60.7v-9.8l0,0c0-3.3-2.7-6-6-6H45.1C41.7,44.899,39.1,47.6,39.1,50.899z"/>
|
||||||
|
<circle cx="61.4" cy="26" r="13.9"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 484 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-three-dots-vertical" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 308 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 677 B After Width: | Height: | Size: 677 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 183 B After Width: | Height: | Size: 183 B |
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="71.753px" height="71.753px" viewBox="0 0 71.753 71.753" style="enable-background:new 0 0 71.753 71.753;"
|
||||||
|
xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path d="M39.798,20.736H28.172v20.738L11.625,41.47V20.736H0L19.899,0.839L39.798,20.736z M51.855,70.914l19.897-19.896H60.129
|
||||||
|
V30.282l-16.547-0.004v20.74H31.957L51.855,70.914z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 873 B |
After Width: | Height: | Size: 797 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 74 B |
Before Width: | Height: | Size: 180 B |
Before Width: | Height: | Size: 73 B |