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/
|
||||
po/*.mo
|
||||
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_QT "Build Qt 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_CLI "Build command-line client" OFF)
|
||||
option(ENABLE_TESTS "Build unit tests" ON)
|
||||
|
@ -622,10 +623,7 @@ if(ENABLE_TESTS)
|
|||
endif()
|
||||
|
||||
function(tr_install_web DST_DIR)
|
||||
install(DIRECTORY ${CMAKE_SOURCE_DIR}/web DESTINATION ${DST_DIR}
|
||||
PATTERN *.am EXCLUDE
|
||||
PATTERN *.in EXCLUDE
|
||||
PATTERN *.scss EXCLUDE)
|
||||
install(DIRECTORY ${CMAKE_SOURCE_DIR}/web/public_html DESTINATION ${DST_DIR})
|
||||
endfunction()
|
||||
|
||||
add_subdirectory(libtransmission)
|
||||
|
@ -637,7 +635,7 @@ if(ENABLE_GTK AND ENABLE_NLS)
|
|||
add_subdirectory(po)
|
||||
endif()
|
||||
|
||||
foreach(P daemon cli utils gtk qt mac)
|
||||
foreach(P cli daemon gtk mac qt utils web)
|
||||
string(TOUPPER "${P}" P_ID)
|
||||
if(ENABLE_${P_ID})
|
||||
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)
|
||||
string(APPEND CPACK_SOURCE_PACKAGE_FILE_NAME "-r${TR_VCS_REVISION}")
|
||||
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")
|
||||
|
||||
## Code Formatting
|
||||
|
|
|
@ -62,7 +62,10 @@ fi
|
|||
cd "${root}/web" || exit 1
|
||||
if [ -n "$fix" ]; then
|
||||
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'
|
||||
exitcode=1
|
||||
fi
|
||||
|
|
|
@ -53,7 +53,7 @@ png2ico(Transmission.ico
|
|||
"${ICONS_DIR}/192x192/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(QTQMSRCDIR "${TR_QT_DIR}/translations")
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
<Merge Id="VCRedist" SourceFile="$(var.MsvcCrtMsmFile)" DiskId="1" Language="0" />
|
||||
<Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
|
||||
<Directory Id="INSTALLDIR" Name="Transmission">
|
||||
<Directory Id="WEBINSTALLDIR" Name="web" />
|
||||
<Directory Id="WEBINSTALLDIR" Name="public_html" />
|
||||
</Directory>
|
||||
</Directory>
|
||||
<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. */
|
||||
|
||||
/* 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))
|
||||
{
|
||||
|
@ -511,7 +511,7 @@ char const* tr_getWebClientDir(tr_session const* session)
|
|||
CFRelease(appRef);
|
||||
|
||||
/* 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))
|
||||
{
|
||||
|
@ -628,7 +628,7 @@ char const* tr_getWebClientDir(tr_session const* session)
|
|||
/* walk through the candidates & look for a match */
|
||||
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);
|
||||
|
||||
if (found)
|
||||
|
|
|
@ -279,7 +279,8 @@ static char const* mimetype_guess(char const* path)
|
|||
{ "html", "text/html" },
|
||||
{ "ico", "image/vnd.microsoft.icon" },
|
||||
{ "js", "application/javascript" },
|
||||
{ "png", "image/png" }
|
||||
{ "png", "image/png" },
|
||||
{ "svg", "image/svg+xml" }
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
"repository": "https://github.com/transmission/transmission",
|
||||
"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": {
|
||||
"lint": "prettier --loglevel warn --check javascript/*js && eslint javascript/*js",
|
||||
"lint:fix": "prettier --loglevel warn -w javascript/*js && eslint --fix javascript/*js"
|
||||
"build": "webpack --config webpack.config.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": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/eslint-parser": "^7.11.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"eslint": "^7.9.0",
|
||||
"prettier": "^2.1.2"
|
||||
"css-loader": "^4.3.0",
|
||||
"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 |