From be219ddee01ed8024a4861828b275923fc9e6a02 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Sun, 13 Sep 2020 21:41:32 -0500 Subject: [PATCH] chore: add precommit hook for testing code style (#1448) * chore: add precommit hook for testing code style * chore: use prettier + eslint for js code --- .jsbeautifyrc | 10 - CMakeLists.txt | 55 +- code_style.sh | 79 +- docker-compose.yml | 1 + docker/code_style/Dockerfile | 5 +- format/eslint.config.json | 24 + format/format.sh | 140 + format/pre-commit | 6 + format/prettier.config.json | 19 + format/skipfiles.txt | 12 + uncrustify.cfg => format/uncrustify.cfg | 31 +- gtk/util.h | 2 +- tests/libtransmission/variant-test.cc | 2 +- web/javascript/common.js | 275 +- web/javascript/dialog.js | 201 +- web/javascript/file-row.js | 339 +- web/javascript/formatter.js | 531 ++-- web/javascript/inspector.js | 1788 ++++++----- web/javascript/main.js | 78 +- web/javascript/notifications.js | 72 +- web/javascript/polyfill.js | 140 +- web/javascript/prefs-dialog.js | 536 ++-- web/javascript/remote.js | 554 ++-- web/javascript/torrent-row.js | 703 +++-- web/javascript/torrent.js | 961 +++--- web/javascript/transmission.js | 3803 ++++++++++++----------- 26 files changed, 5368 insertions(+), 4999 deletions(-) delete mode 100644 .jsbeautifyrc create mode 100644 format/eslint.config.json create mode 100755 format/format.sh create mode 100755 format/pre-commit create mode 100644 format/prettier.config.json create mode 100644 format/skipfiles.txt rename uncrustify.cfg => format/uncrustify.cfg (95%) diff --git a/.jsbeautifyrc b/.jsbeautifyrc deleted file mode 100644 index 87f70e067..000000000 --- a/.jsbeautifyrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "indent_size": 4, - "indent_char": " ", - "indent_level": 0, - "indent_with_tabs": false, - "preserve_newlines": true, - "max_preserve_newlines": 2, - "end_with_newline": true, - "jslint_happy": true -} diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c1076227..e65877dfe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,23 +61,24 @@ endif() set(TR_VCS_REVISION_FILE "${CMAKE_SOURCE_DIR}/REVISION") +if(IS_DIRECTORY ${CMAKE_SOURCE_DIR}/.git) + find_package(Git) +endif() + if(NOT "$ENV{JENKINS_URL}" STREQUAL "" AND NOT "$ENV{GIT_COMMIT}" STREQUAL "") set(TR_VCS_REVISION "$ENV{GIT_COMMIT}") elseif(NOT "$ENV{TEAMCITY_PROJECT_NAME}" STREQUAL "" AND NOT "$ENV{BUILD_VCS_NUMBER}" STREQUAL "") set(TR_VCS_REVISION "$ENV{BUILD_VCS_NUMBER}") -elseif(IS_DIRECTORY ${CMAKE_SOURCE_DIR}/.git) - find_package(Git) - if(GIT_FOUND) - execute_process( - COMMAND - ${GIT_EXECUTABLE} rev-list --max-count=1 HEAD - WORKING_DIRECTORY - ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE - TR_VCS_REVISION - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - endif() +elseif(GIT_FOUND) + execute_process( + COMMAND + ${GIT_EXECUTABLE} rev-list --max-count=1 HEAD + WORKING_DIRECTORY + ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE + TR_VCS_REVISION + OUTPUT_STRIP_TRAILING_WHITESPACE + ) endif() if("${TR_VCS_REVISION}" STREQUAL "" AND EXISTS "${TR_VCS_REVISION_FILE}") @@ -664,4 +665,32 @@ set(CPACK_SOURCE_IGNORE_FILES \\\\.git ) +## Code Formatting + +if(GIT_FOUND) + message(STATUS "Looking for uncrustify") + execute_process(COMMAND + "${GIT_EXECUTABLE}" rev-parse --show-toplevel + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE TR_GIT_ROOT + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(TR_GIT_ROOT) + configure_file( + "${CMAKE_SOURCE_DIR}/format/pre-commit" + "${TR_GIT_ROOT}/.git/hooks/pre-commit" + COPYONLY + ) + add_custom_target(check-format + COMMAND "${CMAKE_SOURCE_DIR}/format/format.sh" --all --check + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + ) + add_custom_target(format + COMMAND "${CMAKE_SOURCE_DIR}/format/format.sh" --all + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + ) + endif(TR_GIT_ROOT) + unset(TR_GIT_ROOT) +endif(GIT_FOUND) + include(CPack) diff --git a/code_style.sh b/code_style.sh index 94836b207..2c518c2c5 100755 --- a/code_style.sh +++ b/code_style.sh @@ -1,79 +1,4 @@ #!/usr/bin/env bash -set -euo pipefail - -[ -z "${1:-}" ] || cd "$1" - -echo '==================' -echo '=== uncrustify ===' -echo '==================' -echo '' - -find \ - cli \ - daemon \ - gtk \ - libtransmission \ - utils \ - \( -name '*.c' -o -name '*.h' \) \ - ! \( -name 'ConvertUTF.*' -o -name 'jsonsl.*' -o -name 'wildmat.c' \) \ - -print0 | -xargs \ - -0 \ - uncrustify \ - --replace \ - --no-backup \ - -c uncrustify.cfg - -find \ - qt \ - tests \ - \( -name '*.cc' -o -name '*.h' \) \ - -print0 | -xargs \ - -0 \ - uncrustify \ - --replace \ - --no-backup \ - -l CPP \ - -c uncrustify.cfg - -echo '' -echo '=================================================================' -echo '=== const placement (until uncrustify supports it, hopefully) ===' -echo '=================================================================' -echo '' - -find \ - cli \ - daemon \ - gtk \ - libtransmission \ - qt \ - utils \ - \( -name '*.c' -o -name '*.cc' -o -name '*.h' \) \ - ! \( -name 'ConvertUTF.*' -o -name 'jsonsl.*' -o -name 'wildmat.c' \) \ - -print0 | -xargs \ - -0 \ - -n1 \ - perl \ - -pi \ - -e 'BEGIN { print STDOUT "Processing: ${ARGV[0]}\n" } s/((?:^|[(,;]|\bstatic\s+)\s*)\b(const)\b(?!\s+\w+\s*\[)/\1>\2/dev/null 2>&1 && pwd )" +exec "${DIR}/format/format.sh" --all diff --git a/docker-compose.yml b/docker-compose.yml index fe49716c9..aa6ae9e6e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,4 +6,5 @@ services: volumes: - .:/src - ./code_style.sh:/code_style.sh:ro + - ./format:/format:ro command: ["/bin/sh", "/code_style.sh", "/src"] diff --git a/docker/code_style/Dockerfile b/docker/code_style/Dockerfile index 1c1ec57ad..844530b07 100644 --- a/docker/code_style/Dockerfile +++ b/docker/code_style/Dockerfile @@ -1,8 +1,11 @@ FROM alpine RUN apk add --no-cache \ + bash \ + git \ npm \ perl \ uncrustify \ && npm install -g \ - js-beautify + eslint \ + prettier diff --git a/format/eslint.config.json b/format/eslint.config.json new file mode 100644 index 000000000..842d7863c --- /dev/null +++ b/format/eslint.config.json @@ -0,0 +1,24 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es6": true, + "jquery": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 6, + "ecmaFeatures": { + "jsx": true + }, + "sourceType": "module" + }, + "rules": { + "curly": ["error", "all"], + "no-undef": "off", + "no-unused-vars": "off", + "no-var": "off", + "prefer-const": "off", + "semi": ["error", "always"] + } +} diff --git a/format/format.sh b/format/format.sh new file mode 100755 index 000000000..038d99ef4 --- /dev/null +++ b/format/format.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash + + +find_sourcefiles_in_dirs(){ + if [ "$changed" -ne "0" ]; then + files="$(git diff --name-only -- "${@}")"; + elif [ "$staged" -ne "0" ]; then + files="$(git diff --name-only --staged -- "${@}")"; + else + files="$(find "${@}")"; + fi + # remove skipfiles + files="$(echo "${files}" | sort | comm -23 - "${root}/format/skipfiles.txt")" + echo "${files}" +} + + +# globals + +all=0 +changed=0 +check=0 +exitcode=0 +staged=0 +root="$(git rev-parse --show-toplevel)" +eslint_args=(-c "${root}/format/eslint.config.json") +prettier_args=(--config "${root}/format/prettier.config.json" --loglevel warn) +uncrustify_args=(-c "${root}/format/uncrustify.cfg") + +# parse command line + +for var in "$@" +do + case "$var" in + --changed) changed=1;; + --cached|--staged) staged=1;; + --check|--test) check=1;; + --all) all=1;; + esac +done + +if [ "${changed}${staged}${all}" -eq "000" ]; then + echo "usage: $0 {--all|--changed|--staged} [--check]" + exit 1 +fi + +if [ "${check}" -ne "0" ]; then + prettier_args+=(--check); + uncrustify_args+=(--check -q); +else + eslint_args+=(--fix) + prettier_args+=(--write) + uncrustify_args+=(--replace --no-backup) +fi + + +cd "${root}" || exit 1 + +# format C/C++ files +tool='uncrustify' +tool_args=("${uncrustify_args[@]}") +cish_files=() +if ! command -v "${tool}" &> /dev/null; then + echo "skipping $tool (not found)" +else + # C + dirs=(cli daemon gtk libtransmission utils) + filestr=$(find_sourcefiles_in_dirs "${dirs[@]}") # newline-delimited string + filestr=$(echo "$filestr" | grep -e "\.[ch]$") # remove non-C files + IFS=$'\n' read -d '' -ra files <<< "${filestr}"; # convert to array + if [ ${#files[@]} -ne 0 ]; then + "${tool}" "${tool_args[@]}" -l C "${files[@]}" 1>/dev/null || exitcode=1 + cish_files+=("${files[@]}") + fi + + # C++ + dirs=(qt tests) + filestr=$(find_sourcefiles_in_dirs "${dirs[@]}") # newline-delimited string + filestr=$(echo "$filestr" | grep -e "\.cc$" -e "\.h$") # remove non-C++ files + IFS=$'\n' read -d '' -ra files <<< "${filestr}"; # convert to array + if [ ${#files[@]} -ne 0 ]; then + "${tool}" "${tool_args[@]}" -l CPP "${files[@]}" 1>/dev/null || exitcode=1 + cish_files+=("${files[@]}") + fi +fi + +# check const placement. +# do this manually since neither clang-format nor uncrustify do it +if [ ${#cish_files[@]} -ne 0 ]; then + if [ "${check}" -ne "0" ]; then + matches="$(grep --line-number --with-filename -P '((?:^|[(,;]|\bstatic\s+)\s*)\b(const)\b(?!\s+\w+\s*\[)' "${cish_files[@]}")" + if [ -n "${matches}" ]; then + echo 'const in wrong place:' + echo "${matches}" + exitcode=1 + fi + else + perl -pi -e 's/((?:^|[(,;]|\bstatic\s+)\s*)\b(const)\b(?!\s+\w+\s*\[)/\1>\2 /dev/null; then + echo "skipping $tool (not found)" +else + dirs=(web) + filestr=$(find_sourcefiles_in_dirs "${dirs[@]}") # newline-delimited string + filestr=$(echo "$filestr" | grep -e "\.js$") # remove non-JS files + IFS=$'\n' read -d '' -ra files <<< "${filestr}"; # convert to array + if [ ${#files[@]} -ne 0 ]; then + "${tool}" "${tool_args[@]}" "${files[@]}" || exitcode=1 + fi +fi + +# lint JS files +tool='eslint' +tool_args=("${eslint_args[@]}") +if ! command -v "${tool}" &> /dev/null; then + echo "skipping $tool (not found)" +else + dirs=(web) + filestr=$(find_sourcefiles_in_dirs "${dirs[@]}") # newline-delimited string + filestr=$(echo "$filestr" | grep -e "\.js$") # remove non-JS files + IFS=$'\n' read -d '' -ra files <<< "${filestr}"; # convert to array + if [ ${#files[@]} -ne 0 ]; then + "${tool}" "${tool_args[@]}" "${files[@]}" || exitcode=1 + fi +fi + + +exit $exitcode diff --git a/format/pre-commit b/format/pre-commit new file mode 100755 index 000000000..4182c254b --- /dev/null +++ b/format/pre-commit @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +[ -z "$TR_SKIP_HOOKS" ] || exit 0 + +root="$(git rev-parse --show-toplevel)" +exec "${root}/format/format.sh" --staged --test diff --git a/format/prettier.config.json b/format/prettier.config.json new file mode 100644 index 000000000..ddf5309b6 --- /dev/null +++ b/format/prettier.config.json @@ -0,0 +1,19 @@ +{ + "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 +} diff --git a/format/skipfiles.txt b/format/skipfiles.txt new file mode 100644 index 000000000..f33bb1166 --- /dev/null +++ b/format/skipfiles.txt @@ -0,0 +1,12 @@ +libtransmission/ConvertUTF.c +libtransmission/ConvertUTF.h +libtransmission/jsonsl.c +libtransmission/jsonsl.h +libtransmission/wildmat.c +web/javascript/jquery/jquery-migrate.min.js +web/javascript/jquery/jquery.min.js +web/javascript/jquery/jquery.transmenu.js +web/javascript/jquery/jquery.transmenu.min.js +web/javascript/jquery/jquery.ui-contextmenu.js +web/javascript/jquery/jquery.ui-contextmenu.min.js +web/javascript/jquery/jquery-ui.min.js diff --git a/uncrustify.cfg b/format/uncrustify.cfg similarity index 95% rename from uncrustify.cfg rename to format/uncrustify.cfg index aa030fbc8..17009ef53 100644 --- a/uncrustify.cfg +++ b/format/uncrustify.cfg @@ -1,4 +1,4 @@ -# Uncrustify-0.70.1_f +# Uncrustify_d-0.71.0-13-42bfdca8 newlines = auto input_tab_size = 8 output_tab_size = 8 @@ -6,6 +6,7 @@ string_escape_char = 92 string_escape_char2 = 0 string_replace_tab_chars = false tok_split_gte = false +disable_processing_nl_cont = false disable_processing_cmt = " *INDENT-OFF*" enable_processing_cmt = " *INDENT-ON*" enable_digraphs = false @@ -159,6 +160,7 @@ sp_throw_paren = force sp_after_throw = force sp_catch_paren = force sp_oc_catch_paren = ignore +sp_before_oc_proto_list = ignore sp_oc_classname_paren = ignore sp_version_paren = ignore sp_scope_paren = ignore @@ -177,7 +179,7 @@ sp_finally_brace = ignore sp_brace_finally = ignore sp_try_brace = ignore sp_getset_brace = ignore -sp_word_brace = remove +sp_word_brace_init_lst = ignore sp_word_brace_ns = add sp_before_dc = remove sp_after_dc = remove @@ -284,6 +286,7 @@ indent_macro_brace = true indent_member = 4 indent_member_single = true indent_sing_line_comments = 0 +indent_sparen_extra = 0 indent_relative_single_line_comments = false indent_switch_case = 0 indent_switch_break_with_case = false @@ -308,7 +311,9 @@ indent_first_for_expr = false indent_square_nl = false indent_preserve_sql = false indent_align_assign = false +indent_off_after_assign = false indent_align_paren = false +indent_oc_inside_msg_sel = false indent_oc_block = false indent_oc_block_msg = 0 indent_oc_msg_colon = 0 @@ -322,8 +327,11 @@ indent_min_vbrace_open = 0 indent_vbrace_open_on_tabstop = false indent_token_after_brace = false indent_cpp_lambda_body = true +indent_compound_literal_return = true indent_using_block = true indent_ternary_operator = 0 +indent_inside_ternary_operator = false +indent_off_after_return = false indent_off_after_return_new = false indent_single_after_return = true indent_ignore_asm_block = false @@ -367,6 +375,7 @@ nl_brace_else = force nl_elseif_brace = force nl_else_brace = force nl_else_if = remove +nl_before_opening_brace_func_class_def = ignore nl_before_if_closing_paren = remove nl_brace_finally = ignore nl_finally_brace = ignore @@ -433,6 +442,7 @@ nl_func_decl_start_multi_line = false nl_func_def_start_multi_line = false nl_func_decl_args = remove nl_func_def_args = remove +nl_func_call_args = ignore nl_func_decl_args_multi_line = false nl_func_def_args_multi_line = false nl_func_decl_end = remove @@ -445,9 +455,11 @@ nl_func_decl_empty = remove nl_func_def_empty = remove nl_func_call_empty = remove nl_func_call_start = ignore +nl_func_call_end = ignore nl_func_call_start_multi_line = false nl_func_call_args_multi_line = false nl_func_call_end_multi_line = false +nl_func_call_args_multi_line_ignore_closures = false nl_template_start = false nl_template_args = false nl_template_end = false @@ -545,6 +557,10 @@ eat_blanks_before_close_brace = true nl_remove_extra_newlines = 0 nl_after_annotation = ignore nl_between_annotation = ignore +nl_before_whole_file_ifdef = 0 +nl_after_whole_file_ifdef = 0 +nl_before_whole_file_endif = 0 +nl_after_whole_file_endif = 0 pos_arith = trail pos_assign = trail pos_bool = trail @@ -625,6 +641,7 @@ align_asm_colon = false align_oc_msg_colon_span = 0 align_oc_msg_colon_first = false align_oc_decl_colon = false +align_oc_msg_colon_xcode_like = false cmt_width = 0 cmt_reflow_mode = 0 cmt_convert_tab_to_spaces = false @@ -673,6 +690,11 @@ mod_sort_case_sensitive = false mod_sort_import = false mod_sort_using = false mod_sort_include = false +mod_sort_incl_import_prioritize_filename = false +mod_sort_incl_import_prioritize_extensionless = false +mod_sort_incl_import_prioritize_angle_over_quotes = false +mod_sort_incl_import_ignore_extension = false +mod_sort_incl_import_grouping_enabled = false mod_move_case_break = false mod_case_brace = ignore mod_remove_empty_return = false @@ -708,9 +730,12 @@ use_indent_continue_only_once = true indent_cpp_lambda_only_once = true use_sp_after_angle_always = false use_options_overriding_for_qt_macros = false +use_form_feed_no_more_as_whitespace_character = false warn_level_tabs_found_in_verbatim_string_literals = 2 +debug_max_number_of_loops = 0 +debug_line_number_to_protocol = 0 type PtrArrayCompareFunc type SIZE_T type tr_session -# option(s) with 'not default' value: 226 +# option(s) with 'not default' value: 225 # diff --git a/gtk/util.h b/gtk/util.h index 55f3c0df9..3b0b5ce94 100644 --- a/gtk/util.h +++ b/gtk/util.h @@ -43,7 +43,7 @@ extern char const* speed_T_str; { \ static GQuark q; \ \ - if G_UNLIKELY(q == 0) \ + if (G_UNLIKELY(q == 0)) \ { \ q = g_quark_from_static_string(#QN); \ } \ diff --git a/tests/libtransmission/variant-test.cc b/tests/libtransmission/variant-test.cc index 263ddf749..65545c944 100644 --- a/tests/libtransmission/variant-test.cc +++ b/tests/libtransmission/variant-test.cc @@ -267,7 +267,7 @@ TEST_F(VariantTest, bencParseAndReencode) { { " ", false } }; - for (const auto& test : tests) + for (auto const& test : tests) { tr_variant val; char const* end = nullptr; diff --git a/web/javascript/common.js b/web/javascript/common.js index fe494cffd..4ff61068e 100644 --- a/web/javascript/common.js +++ b/web/javascript/common.js @@ -5,53 +5,53 @@ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html */ -var transmission, - dialog, - isMobileDevice = RegExp("(iPhone|iPod|Android)").test(navigator.userAgent), - scroll_timeout; +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' - }); - var 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'); + 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; - }; + 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; - }; -}; + /* 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, ">"); -}; + return text.replace(//g, '>'); +} /** * Many of our text changes are triggered by periodic refreshes @@ -59,51 +59,51 @@ function sanitizeText(text) { * so see if the text actually changed before poking the DOM. */ function setTextContent(e, text) { - if (e && (e.textContent != text)) { - e.textContent = text; - }; -}; + if (e && e.textContent != text) { + e.textContent = text; + } +} /* * Given a numerator and denominator, return a ratio string */ Math.ratio = function (numerator, denominator) { - var result = Math.floor(100 * numerator / denominator) / 100; + 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; - }; + // check for special cases + if (result == Number.POSITIVE_INFINITY || result == Number.NEGATIVE_INFINITY) { + result = -2; + } else if (isNaN(result)) { + result = -1; + } - return result; + return result; }; /** * Round a string of a number to a specified number of decimal places */ Number.prototype.toTruncFixed = function (place) { - var ret = Math.floor(this * Math.pow(10, place)) / Math.pow(10, place); - return ret.toFixed(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, ","); + return this.toString().replace(/\B(?=(?:\d{3})+(?!\d))/g, ','); }; /* * Trim whitespace from a string */ String.prototype.trim = function () { - return this.replace(/^\s*/, "").replace(/\s*$/, ""); + return this.replace(/^\s*/, '').replace(/\s*$/, ''); }; /*** **** Preferences ***/ -function Prefs() {}; +function Prefs() {} Prefs.prototype = {}; Prefs._RefreshRate = 'refresh_rate'; @@ -133,25 +133,25 @@ 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 + 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); - }; + if (!(key in Prefs._Defaults)) { + console.warn("unrecognized preference key '%s'", key); + } - var date = new Date(); - date.setFullYear(date.getFullYear() + 1); - document.cookie = key + "=" + val + "; expires=" + date.toGMTString() + "; path=/"; + const date = new Date(); + date.setFullYear(date.getFullYear() + 1); + document.cookie = key + '=' + val + '; expires=' + date.toGMTString() + '; path=/'; }; /** @@ -161,30 +161,30 @@ Prefs.setValue = function (key, val) { * @param fallback if the option isn't set, return this instead */ Prefs.getValue = function (key, fallback) { - var val; + let val; - if (!(key in Prefs._Defaults)) { - console.warn("unrecognized preference key '%s'", key); - }; + if (!(key in Prefs._Defaults)) { + console.warn("unrecognized preference key '%s'", key); + } - var lines = document.cookie.split(';'); - for (var i = 0, len = lines.length; !val && i < len; ++i) { - var line = lines[i].trim(); - var delim = line.indexOf('='); - if ((delim === key.length) && line.indexOf(key) === 0) { - val = line.substring(delim + 1); - }; - }; + 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; + // 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; }; /** @@ -193,75 +193,48 @@ Prefs.getValue = function (key, fallback) { * @pararm o object to be populated (optional) */ Prefs.getClutchPrefs = function (o) { - if (!o) { - o = {}; - }; - for (var key in Prefs._Defaults) { - o[key] = Prefs.getValue(key, Prefs._Defaults[key]); - }; - return 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) { - var 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; - }); + 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 + ); }); -}; - -/** - * http://blog.stevenlevithan.com/archives/parseuri - * - * parseUri 1.2.2 - * (c) Steven Levithan - * MIT License - */ -function parseUri(str) { - var o = parseUri.options; - var m = o.parser[o.strictMode ? "strict" : "loose"].exec(str); - var uri = {}; - var i = 14; - - while (i--) { - uri[o.key[i]] = m[i] || ""; - }; - - uri[o.q.name] = {}; - uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { - if ($1) { - uri[o.q.name][$1] = $2; - }; - }); - - return uri; -}; - -parseUri.options = { - strictMode: false, - key: ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"], - q: { - name: "queryKey", - parser: /(?:^|&)([^&=]*)=?([^&]*)/g - }, - parser: { - strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, - loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ - } + }); }; diff --git a/web/javascript/dialog.js b/web/javascript/dialog.js index 9e58c9c3e..a6d9d03ed 100644 --- a/web/javascript/dialog.js +++ b/web/javascript/dialog.js @@ -6,113 +6,124 @@ */ function Dialog() { - this.initialize(); -}; + this.initialize(); +} Dialog.prototype = { - + /* + * Constructor + */ + initialize: function () { /* - * Constructor + * Private Interface Variables */ - initialize: function () { + 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; - /* - * 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 + ); + }, - // 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 + * + *--------------------------------------------*/ - /*-------------------------------------------- - * - * E V E N T F U N C T I O N S - * - *--------------------------------------------*/ + executeCallback: function () { + this._callback(); + dialog.hideDialog(); + }, - executeCallback: function () { - this._callback(); - dialog.hideDialog(); - }, + hideDialog: function () { + $('body.dialog_showing').removeClass('dialog_showing'); + this._container.hide(); + transmission.hideMobileAddressbar(); + transmission.updateButtonStates(); + }, - hideDialog: function () { - $('body.dialog_showing').removeClass('dialog_showing'); - this._container.hide(); - transmission.hideMobileAddressbar(); - transmission.updateButtonStates(); - }, + isVisible: function () { + return this._container.is(':visible'); + }, - isVisible: function () { - return this._container.is(':visible'); - }, + onCancelClicked: function (event) { + event.data.dialog.hideDialog(); + }, - onCancelClicked: function (event) { - event.data.dialog.hideDialog(); - }, + onConfirmClicked: function (event) { + event.data.dialog.executeCallback(); + }, - 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 + * + *--------------------------------------------*/ - /*-------------------------------------------- - * - * 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(); + /* + * 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(); + }, }; diff --git a/web/javascript/file-row.js b/web/javascript/file-row.js index 2d96c46d7..51eb4cf76 100644 --- a/web/javascript/file-row.js +++ b/web/javascript/file-row.js @@ -6,200 +6,207 @@ */ function FileRow(torrent, depth, name, indices, even) { - var fields = { - have: 0, - indices: [], - isWanted: true, - priorityLow: false, - priorityNormal: false, - priorityHigh: false, - me: this, - size: 0, - torrent: null - }; + const fields = { + have: 0, + indices: [], + isWanted: true, + priorityLow: false, + priorityNormal: false, + priorityHigh: false, + me: this, + size: 0, + torrent: null, + }; - var elements = { - priority_low_button: null, - priority_normal_button: null, - priority_high_button: null, - progress: null, - root: null - }; + const elements = { + priority_low_button: null, + priority_normal_button: null, + priority_high_button: null, + progress: null, + root: null, + }; - var initialize = function (torrent, depth, name, indices, even) { - fields.torrent = torrent; - fields.indices = indices; - createRow(torrent, depth, name, even); - }; + const initialize = function (torrent, depth, name, indices, even) { + fields.torrent = torrent; + fields.indices = indices; + createRow(torrent, depth, name, even); + }; - var refreshWantedHTML = function () { - var 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 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); + }; - var refreshProgressHTML = function () { - var pct = 100 * (fields.size ? (fields.have / fields.size) : 1.0) - var c = [Transmission.fmt.size(fields.have), ' of ', Transmission.fmt.size(fields.size), ' (', Transmission.fmt.percentString(pct), '%)'].join(''); - setTextContent(elements.progress, c); - }; + 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); + }; - var refreshImpl = function () { - var i, - file, - have = 0, - size = 0, - wanted = false, - low = false, - normal = false, - high = false; + 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; - } - } + // 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.have != have || fields.size != size) { + fields.have = have; + fields.size = size; + refreshProgressHTML(); + } - if (fields.isWanted !== wanted) { - fields.isWanted = wanted; - refreshWantedHTML(); - } + if (fields.isWanted !== wanted) { + fields.isWanted = wanted; + refreshWantedHTML(); + } - if (fields.priorityLow !== low) { - fields.priorityLow = low; - $(elements.priority_low_button).toggleClass('selected', low); - } + 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.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); - } - }; + if (fields.priorityHigh !== high) { + fields.priorityHigh = high; + $(elements.priority_high_button).toggleClass('selected', high); + } + }; - var isDone = function () { - return fields.have >= fields.size; - }; + var isDone = function () { + return fields.have >= fields.size; + }; - var isEditable = function () { - return (fields.torrent.getFileCount() > 1) && !isDone(); - }; + var isEditable = function () { + return fields.torrent.getFileCount() > 1 && !isDone(); + }; - var createRow = function (torrent, depth, name, even) { - var e, root, box; + 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; + 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('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 = '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 = '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.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); + 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); + 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_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; + 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'); + $(root).css('margin-left', '' + depth * 16 + 'px'); - refreshImpl(); - return root; - }; + refreshImpl(); + return root; + }; - var fireWantedChanged = function (do_want) { - $(fields.me).trigger('wantedToggled', [fields.indices, do_want]); - }; + 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 firePriorityChanged = function (priority) { + $(fields.me).trigger('priorityToggled', [fields.indices, priority]); + }; - var fireNameClicked = function () { - $(fields.me).trigger('nameClicked', [fields.me, fields.indices]); - }; + var fireNameClicked = function () { + $(fields.me).trigger('nameClicked', [fields.me, fields.indices]); + }; - /*** - **** PUBLIC - ***/ + /*** + **** PUBLIC + ***/ - this.getElement = function () { - return elements.root; - }; - this.refresh = function () { - refreshImpl(); - }; + this.getElement = function () { + return elements.root; + }; + this.refresh = function () { + refreshImpl(); + }; - initialize(torrent, depth, name, indices, even); -}; + initialize(torrent, depth, name, indices, even); +} diff --git a/web/javascript/formatter.js b/web/javascript/formatter.js index df6320904..10153c7b6 100644 --- a/web/javascript/formatter.js +++ b/web/javascript/formatter.js @@ -6,287 +6,296 @@ */ Transmission.fmt = (function () { - var speed_K = 1000; - var speed_K_str = 'kB/s'; - var speed_M_str = 'MB/s'; - var speed_G_str = 'GB/s'; + const speed_K = 1000; + const speed_K_str = 'kB/s'; + const speed_M_str = 'MB/s'; + const speed_G_str = 'GB/s'; - var size_K = 1000; - var size_B_str = 'B'; - var size_K_str = 'kB'; - var size_M_str = 'MB'; - var size_G_str = 'GB'; - var size_T_str = 'TB'; + 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'; - var mem_K = 1024; - var mem_B_str = 'B'; - var mem_K_str = 'KiB'; - var mem_M_str = 'MiB'; - var mem_G_str = 'GiB'; - var mem_T_str = 'TiB'; + 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 { + 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 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); + }, - /* - * 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(' '); + } - /** - * 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; - var convertedSize; - var 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; + } - 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(' '); + }, - // 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(' '); + } - /** - * 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; - var convertedSize; - var 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; + } - 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(' '); + }, - // 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)); + }, - speedBps: function (Bps) { - return this.speed(this.toKBps(Bps)); - }, + toKBps: function (Bps) { + return Math.floor(Bps / speed_K); + }, - toKBps: function (Bps) { - return Math.floor(Bps / speed_K); - }, + speed: function (KBps) { + let speed = KBps; - speed: function (KBps) { - var speed = KBps; + if (speed <= 999.95) { + // 0 KBps to 999 K + return [speed.toTruncFixed(0), speed_K_str].join(' '); + } - if (speed <= 999.95) { // 0 KBps to 999 K - return [speed.toTruncFixed(0), speed_K_str].join(' '); - } + speed /= speed_K; - 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(' '); + } - 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(' '); + }, - // 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'); - timeInterval: function (seconds) { - var days = Math.floor(seconds / 86400), - hours = Math.floor((seconds % 86400) / 3600), - minutes = Math.floor((seconds % 3600) / 60), - seconds = Math.floor(seconds % 60), - d = days + ' ' + (days > 1 ? 'days' : 'day'), - h = hours + ' ' + (hours > 1 ? 'hours' : 'hour'), - m = minutes + ' ' + (minutes > 1 ? 'minutes' : 'minute'), - 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'; - } - - var myDate = new Date(seconds * 1000); - var now = new Date(); - - var date = ""; - var time = ""; - - var sameYear = now.getFullYear() === myDate.getFullYear(); - var sameMonth = now.getMonth() === myDate.getMonth(); - - var 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(); - } - - var hours = myDate.getHours(); - var period = "AM"; - if (hours > 12) { - hours = hours - 12; - period = "PM"; - } - if (hours === 0) { - hours = 12; - } - if (hours < 10) { - hours = "0" + hours; - } - var minutes = myDate.getMinutes(); - if (minutes < 10) { - minutes = "0" + minutes; - } - var 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) { - var formattedFlags = []; - for (var i = 0, flag; flag = flagStr[i]; ++i) { - var 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("" + flag + ""); - }; - }; - - return formattedFlags.join(''); + 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( + '' + flag + '' + ); + } + } + + return formattedFlags.join(''); + }, + }; })(); diff --git a/web/javascript/inspector.js b/web/javascript/inspector.js index 82dc6bf11..f14cd910f 100644 --- a/web/javascript/inspector.js +++ b/web/javascript/inspector.js @@ -6,866 +6,954 @@ */ function Inspector(controller) { - - var data = { - controller: null, - elements: {}, - torrents: [] - }, - - needsExtraInfo = function (torrents) { - return torrents.some(tor => !tor.hasExtraInfo()); - }, - - refreshTorrents = function (callback) { - var 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) { - var 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 () { - var 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 () { - var 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, - uri, - 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; - } - uri = parseUri(str); - if (uri.protocol == 'http' || uri.protocol == 'https') { - str = encodeURI(str); - setInnerHTML(e.comment_lb, '' + str + ''); - } 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; - }; - }; - var empty_creator = !creator || !creator.length; - var 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) { - var 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) { - var 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) { - var 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) { - var 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) { - var 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 () { - var 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 () { - var 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('
'); - if (torrents.length > 1) { - html.push('
', sanitizeText(tor.getName()), '
'); - } - if (!peers || !peers.length) { - html.push('
'); // firefox won't paint the top border if the div is empty - continue; - } - html.push('', - '', - '', - '', - '', - '', - '', - '', - '', - ''); - for (i = 0; peer = peers[i]; ++i) { - parity = (i % 2) ? 'odd' : 'even'; - html.push('', - '', - '', - '', - '', - '', - '', - '', - ''); - } - html.push('
UpDown%StatusAddressClient
', (peer.isEncrypted ? '
' : '
'), '
', '
', (peer.rateToPeer ? fmt.speedBps(peer.rateToPeer) : ''), '', (peer.rateToClient ? fmt.speedBps(peer.rateToClient) : ''), '', Math.floor(peer.progress * 100), '%', '', fmt.peerStatus(peer.flagStr), '', sanitizeText(peer.address), '', sanitizeText(peer.clientName), '
'); - } - - setInnerHTML(peers_list, html.join('')); - }, - - /**** - ***** TRACKERS PAGE - ****/ - - getAnnounceState = function (tracker) { - var 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) { - - var 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) { - - var 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 () { - var 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('
'); - - if (torrents.length > 1) { - html.push('
', sanitizeText(tor.getName()), '
'); - } - - tier = -1; - trackers = tor.getTrackers(); - for (j = 0; tracker = trackers[j]; ++j) { - if (tier != tracker.tier) { - if (tier !== -1) { // close previous tier - html.push('
'); - } - - tier = tracker.tier; - - html.push('
', - 'Tier ', tier + 1, '
', - ''); - } - - html.push(''); // 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(); - }; - + 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(); + } + }, /**** - ***** PUBLIC FUNCTIONS + ***** GENERAL INFO PAGE ****/ - this.setTorrents = function (torrents) { - var d = data; + 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(); - // 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; + // + // state_lb + // - // periodically ask for updates to the inspector's torrents - clearTimeout(d.refreshTimeout); + if (torrents.length < 1) { + str = none; + } else { + isMixed = false; + allPaused = true; + allFinished = true; - function callback() { - refreshTorrents(rescheduleTimeout); + 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(); + } } - function rescheduleTimeout() { - d.refreshTimeout = setTimeout(callback, 2000); + 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; + } + const uri = new URL(str); + if (uri.protocol == 'http:' || uri.protocol == 'https:') { + str = encodeURI(str); + setInnerHTML(e.comment_lb, '' + str + ''); + } 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('
'); + if (torrents.length > 1) { + html.push('
', sanitizeText(tor.getName()), '
'); + } + if (!peers || !peers.length) { + html.push('
'); // firefox won't paint the top border if the div is empty + continue; + } + html.push( + '', + '', + '', + '', + '', + '', + '', + '', + '', + '' + ); + for (i = 0; (peer = peers[i]); ++i) { + parity = i % 2 ? 'odd' : 'even'; + html.push( + '', + '', + '', + '', + '', + '', + '', + '', + '' + ); + } + html.push('
UpDown%StatusAddressClient
', + peer.isEncrypted + ? '
' + : '
', + '
', + '
', + peer.rateToPeer ? fmt.speedBps(peer.rateToPeer) : '', + '', + peer.rateToClient ? fmt.speedBps(peer.rateToClient) : '', + '', + Math.floor(peer.progress * 100), + '%', + '', + fmt.peerStatus(peer.flagStr), + '', + sanitizeText(peer.address), + '', + sanitizeText(peer.clientName), + '
'); + } + + 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('
'); + + if (torrents.length > 1) { + html.push('
', sanitizeText(tor.getName()), '
'); } - rescheduleTimeout(); - refreshTorrents(); + tier = -1; + trackers = tor.getTrackers(); + for (j = 0; (tracker = trackers[j]); ++j) { + if (tier != tracker.tier) { + if (tier !== -1) { + // close previous tier + html.push('
'); + } - // refresh the inspector's UI - updateInspector(); + tier = tracker.tier; + + html.push( + '
', + 'Tier ', + tier + 1, + '
', + ''); + } + + html.push(''); // 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(); }; - initialize(controller); -}; + /**** + ***** 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); +} diff --git a/web/javascript/main.js b/web/javascript/main.js index c5552fd77..1c3af9610 100644 --- a/web/javascript/main.js +++ b/web/javascript/main.js @@ -6,46 +6,48 @@ */ function main() { - // IE specific fixes here - if (jQuery.browser.msie) { - try { - document.execCommand("BackgroundImageCache", false, true); - } catch (err) {}; + // 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); }; - - if (jQuery.browser.safari) { - // Move search field's margin down for the styled input - document.getElementById("torrent_search").style["margin-top"] = 3; + 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'; + }); + } - 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 dialog controller - dialog = new Dialog(); + // Initialise the main Transmission controller + transmission = new Transmission(); +} - // Initialise the main Transmission controller - transmission = new Transmission(); -}; - -document.addEventListener("DOMContentLoaded", main); +document.addEventListener('DOMContentLoaded', main); diff --git a/web/javascript/notifications.js b/web/javascript/notifications.js index 60dfbf2bb..d096c6b2f 100644 --- a/web/javascript/notifications.js +++ b/web/javascript/notifications.js @@ -1,42 +1,46 @@ -var Notifications = {}; +const Notifications = {}; $(document).ready(function () { - if (!window.webkitNotifications) { - return; - }; + if (!window.webkitNotifications) { + return; + } - var notificationsEnabled = (window.webkitNotifications.checkPermission() === 0) - var toggle = $('#toggle_notifications'); + let notificationsEnabled = window.webkitNotifications.checkPermission() === 0; + const toggle = $('#toggle_notifications'); - toggle.show(); - updateMenuTitle(); - $(transmission).bind('downloadComplete seedingComplete', function (event, torrent) { - if (notificationsEnabled) { - var title = (event.type == 'downloadComplete' ? 'Download' : 'Seeding') + ' complete', - content = torrent.getName(), - notification; + 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); - }; - }); + 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'); - }; + 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(); - }; - }; + Notifications.toggle = function () { + if (window.webkitNotifications.checkPermission() !== 0) { + window.webkitNotifications.requestPermission(function () { + notificationsEnabled = window.webkitNotifications.checkPermission() === 0; + updateMenuTitle(); + }); + } else { + notificationsEnabled = !notificationsEnabled; + updateMenuTitle(); + } + }; }); diff --git a/web/javascript/polyfill.js b/web/javascript/polyfill.js index c77197dd6..1297c2ada 100644 --- a/web/javascript/polyfill.js +++ b/web/javascript/polyfill.js @@ -6,82 +6,82 @@ */ if (!Array.from) { - Array.from = (function () { - var toStr = Object.prototype.toString; - var isCallable = function (fn) { - return typeof fn === 'function' || toStr.call(fn) === '[object Function]'; - }; - var toInteger = function (value) { - var 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)); - }; - var maxSafeInteger = Math.pow(2, 53) - 1; - var toLength = function (value) { - var len = toInteger(value); - return Math.min(Math.max(len, 0), maxSafeInteger); - }; + 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. - var C = this; + // 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). - var items = Object(arrayLike); + // 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"); - } + // 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. - var mapFn = arguments.length > 1 ? arguments[1] : void undefined; - var 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'); - } + // 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]; - } - } + // 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). - var len = toLength(items.length); + // 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). - var A = isCallable(C) ? Object(new C(len)) : new Array(len); + // 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. - var k = 0; - // 17. Repeat, while k < len… (also steps a - h) - var 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; - }; - }()); + // 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; + }; + })(); } diff --git a/web/javascript/prefs-dialog.js b/web/javascript/prefs-dialog.js index 3aeddf080..59b419a72 100644 --- a/web/javascript/prefs-dialog.js +++ b/web/javascript/prefs-dialog.js @@ -6,297 +6,303 @@ */ function PrefsDialog(remote) { + const data = { + dialog: null, + remote: null, + elements: {}, - var 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', + ], - // 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'], + }, + }; - // 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 ' + (is_open ? 'Open' : 'Closed') + ''; + 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); } - }; - - var initTimeDropDown = function (e) { - var 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); + 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'], }; + }; - var onPortChecked = function (response) { - var is_open = response['arguments']['port-is-open']; - var text = 'Port is ' + (is_open ? 'Open' : 'Closed') + ''; - var e = data.elements.root.find('#port-label'); - setInnerHTML(e[0], text); - }; + const initialize = function (remote) { + let i, key, e, o; - var setGroupEnabled = function (parent_key, enabled) { - var i, key, keys, root; + data.remote = remote; - if (parent_key in data.groups) { - root = data.elements.root; - keys = data.groups[parent_key]; + e = $('#prefs-dialog'); + data.elements.root = e; - for (i = 0; key = keys[i]; ++i) { - root.find('#' + key).attr('disabled', !enabled); - }; + 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); - var onBlocklistUpdateClicked = function () { - data.remote.updateBlocklist(); - setBlocklistButtonEnabled(false); - }; + e = e.find('#blocklist-update-button'); + data.elements.blocklist_button = e; + e.click(onBlocklistUpdateClicked); - var setBlocklistButtonEnabled = function (b) { - var e = data.elements.blocklist_button; - e.attr('disabled', !b); - e.val(b ? 'Update' : 'Updating...'); - }; - - var getValue = function (e) { - var str; - - switch (e[0].type) { + // 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': - return e.prop('checked'); + case 'select-one': + e.change(onControlChanged); + break; 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; + e.focus(onControlFocused); + e.blur(onControlBlurred); + break; default: - return null; + 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 callback is for controls whose changes can be applied - immediately, like checkboxs, radioboxes, and selects */ - var onControlChanged = function (ev) { - var o = {}; - o[ev.target.id] = getValue($(ev.target)); - data.remote.savePrefs(o); - }; + this.show = function () { + transmission.hideMobileAddressbar(); - /* 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 */ - var onControlFocused = function (ev) { - data.oldValue = getValue($(ev.target)); - }; + setBlocklistButtonEnabled(true); + data.remote.checkPort(onPortChecked, this); + data.elements.root.dialog('open'); + }; - var onControlBlurred = function (ev) { - var newValue = getValue($(ev.target)); - if (newValue !== data.oldValue) { - var o = {}; - o[ev.target.id] = newValue; - data.remote.savePrefs(o); - delete data.oldValue; - } - }; + this.close = function () { + transmission.hideMobileAddressbar(); + data.elements.root.dialog('close'); + }; - var getDefaultMobileOptions = function () { - return { - width: $(window).width(), - height: $(window).height(), - position: ['left', 'top'] - }; - }; + this.shouldAddedTorrentsStart = function () { + return data.elements.root.find('#start-added-torrents')[0].checked; + }; - var initialize = function (remote) { - var 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); - - default: - break; - }; - }; - }; - - var getValues = function () { - var 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) { - var e, i, key, val; - var keys = data.keys; - var 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); -}; + data.dialog = this; + initialize(remote); +} diff --git a/web/javascript/remote.js b/web/javascript/remote.js index feb147aa9..153309a09 100644 --- a/web/javascript/remote.js +++ b/web/javascript/remote.js @@ -5,282 +5,298 @@ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html */ -var 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' +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; + this.initialize(controller); + return this; } TransmissionRemote.prototype = { - /* - * Constructor - */ - initialize: function (controller) { - this._controller = controller; - this._error = ''; - this._token = ''; - }, + /* + * 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) { - var token; - var remote = this; + /* + * 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(/(<([^>]+)>)/ig, "") : ""; - 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) { - var 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) { - var o = { - method: 'session-get' - }; - this.sendRequest(o, callback, context, async); - }, - - checkPort: function (callback, context, async) { - var o = { - method: 'port-test' - }; - this.sendRequest(o, callback, context, async); - }, - - renameTorrent: function (torrentIds, oldpath, newname, callback, context) { - var o = { - method: 'torrent-rename-path', - arguments: { - 'ids': torrentIds, - 'path': oldpath, - 'name': newname - } - }; - this.sendRequest(o, callback, context); - }, - - loadDaemonStats: function (callback, context, async) { - var o = { - method: 'session-stats' - }; - this.sendRequest(o, callback, context, async); - }, - - updateTorrents: function (torrentIds, fields, callback, context) { - var o = { - method: 'torrent-get', - arguments: { - 'fields': fields - } - }; - if (torrentIds) { - o['arguments'].ids = torrentIds; - }; - this.sendRequest(o, function (response) { - var args = response['arguments']; - callback.call(context, args.torrents, args.removed); - }); - }, - - getFreeSpace: function (dir, callback, context) { - var o = { - method: 'free-space', - arguments: { - path: dir - } - }; - this.sendRequest(o, function (response) { - var args = response['arguments']; - callback.call(context, args.path, args['size-bytes']); - }); - }, - - changeFileCommand: function (torrentId, fileIndices, command) { - var 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; - var 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) { - var 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) { - var remote = this; - var o = { - method: 'torrent-remove', - arguments: { - 'delete-local-data': true, - ids: [] - } - }; - - if (torrents) { - for (var 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) { - var remote = this; - if (url.match(/^[0-9a-f]{40}$/i)) { - url = 'magnet:?xt=urn:btih:' + url; - } - var o = { - method: 'torrent-add', - arguments: { - paused: (options.paused), - filename: url - } - }; - this.sendRequest(o, function () { - remote._controller.refreshTorrents(); - }); - }, - savePrefs: function (args) { - var remote = this; - var o = { - method: 'session-set', - arguments: args - }; - this.sendRequest(o, function () { - remote._controller.loadDaemonPrefs(); - }); - }, - updateBlocklist: function () { - var remote = this; - var 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); + // 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); + }, }; diff --git a/web/javascript/torrent-row.js b/web/javascript/torrent-row.js index a9419e7bc..655149920 100644 --- a/web/javascript/torrent-row.js +++ b/web/javascript/torrent-row.js @@ -8,111 +8,112 @@ function TorrentRendererHelper() {} TorrentRendererHelper.getProgressInfo = function (controller, t) { - var pct, extra; - var s = t.getStatus(); - var seed_ratio_limit = t.seedRatioLimit(controller); + 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 (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 = ''; - }; + 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(' ') - }; + return { + percent: pct, + complete: ['torrent_progress_bar', 'complete', extra].join(' '), + incomplete: ['torrent_progress_bar', 'incomplete', extra].join(' '), + }; }; TorrentRendererHelper.createProgressbar = function (classes) { - var complete, incomplete, progressbar; + let complete, incomplete, progressbar; - complete = document.createElement('div'); - complete.className = 'torrent_progress_bar complete'; + complete = document.createElement('div'); + complete.className = 'torrent_progress_bar complete'; - incomplete = document.createElement('div'); - incomplete.className = 'torrent_progress_bar incomplete'; + 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); + progressbar = document.createElement('div'); + progressbar.className = 'torrent_progress_bar_container ' + classes; + progressbar.appendChild(complete); + progressbar.appendChild(incomplete); - return { - 'element': progressbar, - 'complete': complete, - 'incomplete': incomplete - }; + return { + element: progressbar, + complete: complete, + incomplete: incomplete, + }; }; TorrentRendererHelper.renderProgressbar = function (controller, t, progressbar) { - var e, style, width, display; - var info = TorrentRendererHelper.getProgressInfo(controller, t); + 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 - }); - }; + // 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; - }; + if (e.className !== info.complete) { + e.className = info.complete; + } - // update the incomplete progressbar - e = progressbar.incomplete; - display = (info.percent < 100) ? 'block' : 'none'; + // update the incomplete progressbar + e = progressbar.incomplete; + display = info.percent < 100 ? 'block' : 'none'; - if (e.style.display !== display) { - e.style.display = display; - }; + if (e.style.display !== display) { + e.style.display = display; + } - if (e.className !== info.incomplete) { - e.className = info.incomplete; - }; + if (e.className !== info.incomplete) { + e.className = info.incomplete; + } }; TorrentRendererHelper.formatUL = function (t) { - return '▲' + Transmission.fmt.speedBps(t.getUploadSpeed()); + return '▲' + Transmission.fmt.speedBps(t.getUploadSpeed()); }; TorrentRendererHelper.formatDL = function (t) { - return '▼' + Transmission.fmt.speedBps(t.getDownloadSpeed()); + return '▼' + Transmission.fmt.speedBps(t.getDownloadSpeed()); }; TorrentRendererHelper.formatETA = function (t) { - var eta = t.getETA(); - if (eta < 0 || eta >= (999 * 60 * 60)) { - return ""; - }; - return "ETA: " + Transmission.fmt.timeInterval(eta); + const eta = t.getETA(); + if (eta < 0 || eta >= 999 * 60 * 60) { + return ''; + } + return 'ETA: ' + Transmission.fmt.timeInterval(eta); }; /**** @@ -120,178 +121,208 @@ TorrentRendererHelper.formatETA = function (t) { ***** ****/ -function TorrentRendererFull() {}; +function TorrentRendererFull() {} TorrentRendererFull.prototype = { - createRow: function () { - var root, name, peers, progressbar, details, image, button; + createRow: function () { + let root, name, peers, progressbar, details, image, button; - root = document.createElement('li'); - root.className = 'torrent'; + root = document.createElement('li'); + root.className = 'torrent'; - name = document.createElement('div'); - name.className = 'torrent_name'; + name = document.createElement('div'); + name.className = 'torrent_name'; - peers = document.createElement('div'); - peers.className = 'torrent_peer_details'; + peers = document.createElement('div'); + peers.className = 'torrent_peer_details'; - progressbar = TorrentRendererHelper.createProgressbar('full'); + progressbar = TorrentRendererHelper.createProgressbar('full'); - details = document.createElement('div'); - details.className = 'torrent_progress_details'; + details = document.createElement('div'); + details.className = 'torrent_progress_details'; - image = document.createElement('div'); - button = document.createElement('a'); - button.appendChild(image); + 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.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; + 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; - }, + return root; + }, - getPeerDetails: function (t) { - var err, - peer_count, - webseed_count, - fmt = Transmission.fmt; + 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()) { - var MetaDataStatus = "retrieving"; - if (t.isStopped()) { - MetaDataStatus = "needs"; - }; - var percent = 100 * t.getMetadataPercentComplete(); - return ["Magnetized transfer - " + MetaDataStatus + " metadata (", - Transmission.fmt.percentString(percent), - "%)" - ].join(''); - } - - var c; - var sizeWhenDone = t.getSizeWhenDone(); - var totalSize = t.getTotalSize(); - var 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(' - '); - var 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 - var has_error = t.getError() !== Torrent._ErrNone; - var 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 - var is_stopped = t.isStopped(); - e = root._pause_resume_button_image; - e.alt = is_stopped ? 'Resume' : 'Pause'; - e.className = is_stopped ? 'torrent_resume' : 'torrent_pause'; + 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'; + }, }; /**** @@ -299,79 +330,84 @@ TorrentRendererFull.prototype = { ***** ****/ -function TorrentRendererCompact() {}; +function TorrentRendererCompact() {} TorrentRendererCompact.prototype = { - createRow: function () { - var progressbar, details, name, root; + createRow: function () { + let progressbar, details, name, root; - progressbar = TorrentRendererHelper.createProgressbar('compact'); + progressbar = TorrentRendererHelper.createProgressbar('compact'); - details = document.createElement('div'); - details.className = 'torrent_peer_details compact'; + details = document.createElement('div'); + details.className = 'torrent_peer_details compact'; - name = document.createElement('div'); - name.className = 'torrent_name 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; - }, + 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) { - var c; - if ((c = t.getErrorMessage())) { - return c; - }; - if (t.isDownloading()) { - var have_dn = t.getDownloadSpeed() > 0; - var have_up = t.getUploadSpeed() > 0; - - if (!have_up && !have_dn) { - return 'Idle'; - }; - var 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 - var is_stopped = t.isStopped(); - var e = root._name_container; - $(e).toggleClass('paused', is_stopped); - setTextContent(e, t.getName()); - - // peer details - var 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); + 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); + }, }; /**** @@ -380,37 +416,36 @@ TorrentRendererCompact.prototype = { ****/ function TorrentRow(view, controller, torrent) { - this.initialize(view, controller, torrent); -}; + this.initialize(view, controller, torrent); +} TorrentRow.prototype = { - initialize: function (view, controller, torrent) { - var 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) { - var 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(); + 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(); + }, }; diff --git a/web/javascript/torrent.js b/web/javascript/torrent.js index c027c2f04..6abc9f076 100644 --- a/web/javascript/torrent.js +++ b/web/javascript/torrent.js @@ -6,8 +6,8 @@ */ function Torrent(data) { - this.initialize(data); -}; + this.initialize(data); +} /*** **** @@ -46,64 +46,60 @@ 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' -]; +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' + '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' + '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' + 'activityDate', + 'corruptEver', + 'desiredAvailable', + 'downloadedEver', + 'fileStats', + 'haveUnchecked', + 'haveValid', + 'peers', + 'startDate', + 'trackerStats', ]; /*** @@ -113,370 +109,373 @@ Torrent.Fields.StatsExtra = [ ***/ Torrent.prototype = { - initialize: function (data) { - this.fields = {}; - this.fieldObservers = {}; - this.refresh(data); - }, + 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); - }, + notifyOnFieldChange: function (field, callback) { + this.fieldObservers[field] = this.fieldObservers[field] || []; + this.fieldObservers[field].push(callback); + }, - setField: function (o, name, value) { - var i, observer; + 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) { - var changed = false; - var myfiles = this.fields.files || []; - var keys = ['length', 'name', 'bytesCompleted', 'wanted', 'priority']; - var 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) { - var i, t, announces = []; - - for (i = 0; t = trackers[i]; ++i) { - announces.push(t.announce.toLowerCase()); - }; - return announces.join('\t'); - }, - - refreshFields: function (data) { - var key; - var 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 () { - var 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 () { - var f = this.fields; - if (!f.collatedName && f.name) { - f.collatedName = f.name.toLowerCase(); - }; - return f.collatedName || ''; - }, - getCollatedTrackers: function () { - var f = this.fields; - if (!f.collatedTrackers && f.trackers) { - f.collatedTrackers = this.collateTrackers(f.trackers); - }; - return f.collatedTrackers || ''; - }, - - /**** - ***** - ****/ - - testState: function (state) { - var 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... - var 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; + 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; + }, }; /*** @@ -486,92 +485,92 @@ Torrent.prototype = { ***/ Torrent.compareById = function (ta, tb) { - return ta.getId() - tb.getId(); + return ta.getId() - tb.getId(); }; Torrent.compareByName = function (ta, tb) { - return ta.getCollatedName().localeCompare(tb.getCollatedName()) || Torrent.compareById(ta, tb); + return ta.getCollatedName().localeCompare(tb.getCollatedName()) || Torrent.compareById(ta, tb); }; Torrent.compareByQueue = function (ta, tb) { - return ta.getQueuePosition() - tb.getQueuePosition(); + return ta.getQueuePosition() - tb.getQueuePosition(); }; Torrent.compareByAge = function (ta, tb) { - var a = ta.getDateAdded(); - var b = tb.getDateAdded(); + const a = ta.getDateAdded(); + const b = tb.getDateAdded(); - return (b - a) || Torrent.compareByQueue(ta, tb); + return b - a || Torrent.compareByQueue(ta, tb); }; Torrent.compareByState = function (ta, tb) { - var a = ta.getStatus(); - var b = tb.getStatus(); + const a = ta.getStatus(); + const b = tb.getStatus(); - return (b - a) || Torrent.compareByQueue(ta, tb); + return b - a || Torrent.compareByQueue(ta, tb); }; Torrent.compareByActivity = function (ta, tb) { - var a = ta.getActivity(); - var b = tb.getActivity(); + const a = ta.getActivity(); + const b = tb.getActivity(); - return (b - a) || Torrent.compareByState(ta, tb); + return b - a || Torrent.compareByState(ta, tb); }; Torrent.compareByRatio = function (ta, tb) { - var a = ta.getUploadRatio(); - var b = tb.getUploadRatio(); + const a = ta.getUploadRatio(); + const b = tb.getUploadRatio(); - if (a < b) { - return 1; - }; - if (a > b) { - return -1; - }; - return Torrent.compareByState(ta, tb); + if (a < b) { + return 1; + } + if (a > b) { + return -1; + } + return Torrent.compareByState(ta, tb); }; Torrent.compareByProgress = function (ta, tb) { - var a = ta.getPercentDone(); - var b = tb.getPercentDone(); + const a = ta.getPercentDone(); + const b = tb.getPercentDone(); - return (a - b) || Torrent.compareByRatio(ta, tb); + return a - b || Torrent.compareByRatio(ta, tb); }; Torrent.compareBySize = function (ta, tb) { - var a = ta.getTotalSize(); - var b = tb.getTotalSize(); + const a = ta.getTotalSize(); + const b = tb.getTotalSize(); - return (a - b) || Torrent.compareByName(ta, tb); + return a - b || Torrent.compareByName(ta, tb); }; Torrent.compareTorrents = function (a, b, sortMethod, sortDirection) { - var i; + let i; - switch (sortMethod) { + switch (sortMethod) { case Prefs._SortByActivity: - i = Torrent.compareByActivity(a, b); - break; + i = Torrent.compareByActivity(a, b); + break; case Prefs._SortByAge: - i = Torrent.compareByAge(a, b); - break; + i = Torrent.compareByAge(a, b); + break; case Prefs._SortByQueue: - i = Torrent.compareByQueue(a, b); - break; + i = Torrent.compareByQueue(a, b); + break; case Prefs._SortByProgress: - i = Torrent.compareByProgress(a, b); - break; + i = Torrent.compareByProgress(a, b); + break; case Prefs._SortBySize: - i = Torrent.compareBySize(a, b); - break; + i = Torrent.compareBySize(a, b); + break; case Prefs._SortByState: - i = Torrent.compareByState(a, b); - break; + i = Torrent.compareByState(a, b); + break; case Prefs._SortByRatio: - i = Torrent.compareByRatio(a, b); - break; + i = Torrent.compareByRatio(a, b); + break; default: - i = Torrent.compareByName(a, b); - break; - }; + i = Torrent.compareByName(a, b); + break; + } - if (sortDirection === Prefs._SortDescending) { - i = -i; - }; + if (sortDirection === Prefs._SortDescending) { + i = -i; + } - return i; + return i; }; /** @@ -580,36 +579,36 @@ Torrent.compareTorrents = function (a, b, sortMethod, sortDirection) { * @param sortDirection Prefs._SortAscending or Prefs._SortDescending */ Torrent.sortTorrents = function (torrents, sortMethod, sortDirection) { - switch (sortMethod) { + switch (sortMethod) { case Prefs._SortByActivity: - torrents.sort(this.compareByActivity); - break; + torrents.sort(this.compareByActivity); + break; case Prefs._SortByAge: - torrents.sort(this.compareByAge); - break; + torrents.sort(this.compareByAge); + break; case Prefs._SortByQueue: - torrents.sort(this.compareByQueue); - break; + torrents.sort(this.compareByQueue); + break; case Prefs._SortByProgress: - torrents.sort(this.compareByProgress); - break; + torrents.sort(this.compareByProgress); + break; case Prefs._SortBySize: - torrents.sort(this.compareBySize); - break; + torrents.sort(this.compareBySize); + break; case Prefs._SortByState: - torrents.sort(this.compareByState); - break; + torrents.sort(this.compareByState); + break; case Prefs._SortByRatio: - torrents.sort(this.compareByRatio); - break; + torrents.sort(this.compareByRatio); + break; default: - torrents.sort(this.compareByName); - break; - }; + torrents.sort(this.compareByName); + break; + } - if (sortDirection === Prefs._SortDescending) { - torrents.reverse(); - }; + if (sortDirection === Prefs._SortDescending) { + torrents.reverse(); + } - return torrents; + return torrents; }; diff --git a/web/javascript/transmission.js b/web/javascript/transmission.js index 93abfe751..f1647585c 100644 --- a/web/javascript/transmission.js +++ b/web/javascript/transmission.js @@ -6,1913 +6,1958 @@ */ function Transmission() { - this.initialize(); + this.initialize(); } Transmission.prototype = { - /**** - ***** - ***** STARTUP - ***** - ****/ + /**** + ***** + ***** STARTUP + ***** + ****/ - initialize: function () { - var e; + initialize: function () { + let e; - // Initialize the helper classes - this.remote = new TransmissionRemote(this); - this.inspector = new Inspector(this); - this.prefsDialog = new PrefsDialog(this.remote); - $(this.prefsDialog).bind('closed', $.proxy(this.onPrefsDialogClosed, this)); + // Initialize the helper classes + this.remote = new TransmissionRemote(this); + this.inspector = new Inspector(this); + this.prefsDialog = new PrefsDialog(this.remote); + $(this.prefsDialog).bind('closed', $.proxy(this.onPrefsDialogClosed, this)); - this.isMenuEnabled = !isMobileDevice; + this.isMenuEnabled = !isMobileDevice; - // Initialize the implementation fields - this.filterText = ''; - this._torrents = {}; - this._rows = []; - this.dirtyTorrents = {}; - this.uriCache = {}; + // Initialize the implementation fields + this.filterText = ''; + this._torrents = {}; + this._rows = []; + this.dirtyTorrents = {}; + this.uriCache = {}; - // Initialize the clutch preferences - Prefs.getClutchPrefs(this); + // Initialize the clutch preferences + Prefs.getClutchPrefs(this); - // Set up user events - $('#toolbar-pause').click($.proxy(this.stopSelectedClicked, this)); - $('#toolbar-start').click($.proxy(this.startSelectedClicked, this)); - $('#toolbar-pause-all').click($.proxy(this.stopAllClicked, this)); - $('#toolbar-start-all').click($.proxy(this.startAllClicked, this)); - $('#toolbar-remove').click($.proxy(this.removeClicked, this)); - $('#toolbar-open').click($.proxy(this.openTorrentClicked, this)); + // Set up user events + $('#toolbar-pause').click($.proxy(this.stopSelectedClicked, this)); + $('#toolbar-start').click($.proxy(this.startSelectedClicked, this)); + $('#toolbar-pause-all').click($.proxy(this.stopAllClicked, this)); + $('#toolbar-start-all').click($.proxy(this.startAllClicked, this)); + $('#toolbar-remove').click($.proxy(this.removeClicked, this)); + $('#toolbar-open').click($.proxy(this.openTorrentClicked, this)); - $('#prefs-button').click($.proxy(this.togglePrefsDialogClicked, this)); + $('#prefs-button').click($.proxy(this.togglePrefsDialogClicked, this)); - $('#upload_confirm_button').click($.proxy(this.confirmUploadClicked, this)); - $('#upload_cancel_button').click($.proxy(this.hideUploadDialog, this)); + $('#upload_confirm_button').click($.proxy(this.confirmUploadClicked, this)); + $('#upload_cancel_button').click($.proxy(this.hideUploadDialog, this)); - $('#rename_confirm_button').click($.proxy(this.confirmRenameClicked, this)); - $('#rename_cancel_button').click($.proxy(this.hideRenameDialog, this)); + $('#rename_confirm_button').click($.proxy(this.confirmRenameClicked, this)); + $('#rename_cancel_button').click($.proxy(this.hideRenameDialog, this)); - $('#move_confirm_button').click($.proxy(this.confirmMoveClicked, this)); - $('#move_cancel_button').click($.proxy(this.hideMoveDialog, this)); + $('#move_confirm_button').click($.proxy(this.confirmMoveClicked, this)); + $('#move_cancel_button').click($.proxy(this.hideMoveDialog, this)); - $('#turtle-button').click($.proxy(this.toggleTurtleClicked, this)); - $('#compact-button').click($.proxy(this.toggleCompactClicked, this)); + $('#turtle-button').click($.proxy(this.toggleTurtleClicked, this)); + $('#compact-button').click($.proxy(this.toggleCompactClicked, this)); - // tell jQuery to copy the dataTransfer property from events over if it exists - jQuery.event.props.push("dataTransfer"); + // tell jQuery to copy the dataTransfer property from events over if it exists + jQuery.event.props.push('dataTransfer'); - $('#torrent_upload_form').submit(function () { - $('#upload_confirm_button').click(); - return false; - }); + $('#torrent_upload_form').submit(function () { + $('#upload_confirm_button').click(); + return false; + }); - $('#toolbar-inspector').click($.proxy(this.toggleInspector, this)); + $('#toolbar-inspector').click($.proxy(this.toggleInspector, this)); - e = $('#filter-mode'); - e.val(this[Prefs._FilterMode]); - e.change($.proxy(this.onFilterModeClicked, this)); - $('#filter-tracker').change($.proxy(this.onFilterTrackerClicked, this)); + e = $('#filter-mode'); + e.val(this[Prefs._FilterMode]); + e.change($.proxy(this.onFilterModeClicked, this)); + $('#filter-tracker').change($.proxy(this.onFilterTrackerClicked, this)); - if (!isMobileDevice) { - $(document).bind('keydown', $.proxy(this.keyDown, this)); - $(document).bind('keyup', $.proxy(this.keyUp, this)); - $('#torrent_container').click($.proxy(this.deselectAll, this)); - $('#torrent_container').bind('dragover', $.proxy(this.dragenter, this)); - $('#torrent_container').bind('dragenter', $.proxy(this.dragenter, this)); - $('#torrent_container').bind('drop', $.proxy(this.drop, this)); - $('#inspector_link').click($.proxy(this.toggleInspector, this)); + if (!isMobileDevice) { + $(document).bind('keydown', $.proxy(this.keyDown, this)); + $(document).bind('keyup', $.proxy(this.keyUp, this)); + $('#torrent_container').click($.proxy(this.deselectAll, this)); + $('#torrent_container').bind('dragover', $.proxy(this.dragenter, this)); + $('#torrent_container').bind('dragenter', $.proxy(this.dragenter, this)); + $('#torrent_container').bind('drop', $.proxy(this.drop, this)); + $('#inspector_link').click($.proxy(this.toggleInspector, this)); - this.setupSearchBox(); - this.createContextMenu(); - }; + this.setupSearchBox(); + this.createContextMenu(); + } - if (this.isMenuEnabled) { - this.createSettingsMenu(); - }; + if (this.isMenuEnabled) { + this.createSettingsMenu(); + } - e = {}; - e.torrent_list = $('#torrent_list')[0]; - e.toolbar_buttons = $('#toolbar ul li'); - e.toolbar_pause_button = $('#toolbar-pause')[0]; - e.toolbar_start_button = $('#toolbar-start')[0]; - e.toolbar_remove_button = $('#toolbar-remove')[0]; - this.elements = e; + e = {}; + e.torrent_list = $('#torrent_list')[0]; + e.toolbar_buttons = $('#toolbar ul li'); + e.toolbar_pause_button = $('#toolbar-pause')[0]; + e.toolbar_start_button = $('#toolbar-start')[0]; + e.toolbar_remove_button = $('#toolbar-remove')[0]; + this.elements = e; - // Apply the prefs settings to the gui - this.initializeSettings(); + // Apply the prefs settings to the gui + this.initializeSettings(); - // Get preferences & torrents from the daemon - var async = false; - this.loadDaemonPrefs(async); - this.loadDaemonStats(async); - this.initializeTorrents(); - this.refreshTorrents(); - this.togglePeriodicSessionRefresh(true); + // Get preferences & torrents from the daemon + const async = false; + this.loadDaemonPrefs(async); + this.loadDaemonStats(async); + this.initializeTorrents(); + this.refreshTorrents(); + this.togglePeriodicSessionRefresh(true); - this.updateButtonsSoon(); - }, + this.updateButtonsSoon(); + }, - loadDaemonPrefs: function (async, callback) { - this.remote.loadDaemonPrefs(function (data) { - var o = data['arguments']; - Prefs.getClutchPrefs(o); - this.updateGuiFromSession(o); - this.sessionProperties = o; + loadDaemonPrefs: function (async, callback) { + this.remote.loadDaemonPrefs( + function (data) { + const o = data['arguments']; + Prefs.getClutchPrefs(o); + this.updateGuiFromSession(o); + this.sessionProperties = o; - if (callback) { - callback(); - } - }, this, async); - }, - - loadImages: function () { - for (var i = 0, row; row = arguments[i]; ++i) { - jQuery("").attr("src", row); - }; - }, - - /* - * Load the clutch prefs and init the GUI according to those prefs - */ - initializeSettings: function () { - Prefs.getClutchPrefs(this); - - if (this.isMenuEnabled) { - $('#sort_by_' + this[Prefs._SortMethod]).selectMenuItem(); - - if (this[Prefs._SortDirection] === Prefs._SortDescending) { - $('#reverse_sort_order').selectMenuItem(); - }; + if (callback) { + callback(); } - - this.initCompactMode(); - }, - - /* - * Set up the search box - */ - setupSearchBox: function () { - var tr = this; - var search_box = $('#torrent_search'); - search_box.bind('keyup click', function () { - tr.setFilterText(this.value); - }); - if (!$.browser.safari) { - search_box.addClass('blur'); - search_box[0].value = 'Filter'; - search_box.bind('blur', function () { - if (this.value === '') { - $(this).addClass('blur'); - this.value = 'Filter'; - tr.setFilterText(null); - }; - }).bind('focus', function () { - if ($(this).is('.blur')) { - this.value = ''; - $(this).removeClass('blur'); - } - }); - } - }, - - /** - * Create the torrent right-click menu - */ - createContextMenu: function () { - var tr = this; - var bindings = { - pause_selected: function () { - tr.stopSelectedTorrents(); - }, - resume_selected: function () { - tr.startSelectedTorrents(false); - }, - resume_now_selected: function () { - tr.startSelectedTorrents(true); - }, - move: function () { - tr.moveSelectedTorrents(false); - }, - remove: function () { - tr.removeSelectedTorrents(); - }, - remove_data: function () { - tr.removeSelectedTorrentsAndData(); - }, - verify: function () { - tr.verifySelectedTorrents(); - }, - rename: function () { - tr.renameSelectedTorrents(); - }, - reannounce: function () { - tr.reannounceSelectedTorrents(); - }, - move_top: function () { - tr.moveTop(); - }, - move_up: function () { - tr.moveUp(); - }, - move_down: function () { - tr.moveDown(); - }, - move_bottom: function () { - tr.moveBottom(); - }, - select_all: function () { - tr.selectAll(); - }, - deselect_all: function () { - tr.deselectAll(); - } - }; - - // Set up the context menu - $("ul#torrent_list").contextmenu({ - delegate: ".torrent", - menu: "#torrent_context_menu", - preventSelect: true, - taphold: true, - show: { - effect: "none" - }, - hide: { - effect: "none" - }, - select: function (event, ui) { - bindings[ui.cmd](); - }, - beforeOpen: $.proxy(function (event, ui) { - var element = $(event.currentTarget); - var i = $('#torrent_list > li').index(element); - if ((i !== -1) && !this._rows[i].isSelected()) { - this.setSelectedRow(this._rows[i]); - }; - - this.calculateTorrentStates(function (s) { - var tl = $(event.target); - tl.contextmenu("enableEntry", "pause_selected", s.activeSel > 0); - tl.contextmenu("enableEntry", "resume_selected", s.pausedSel > 0); - tl.contextmenu("enableEntry", "resume_now_selected", s.pausedSel > 0 || s.queuedSel > 0); - tl.contextmenu("enableEntry", "rename", s.sel == 1); - }); - }, this) - }); - }, - - createSettingsMenu: function () { - $("#footer_super_menu").transMenu({ - open: function () { - $("#settings_menu").addClass("selected"); - }, - close: function () { - $("#settings_menu").removeClass("selected"); - }, - select: $.proxy(this.onMenuClicked, this) - }); - $("#settings_menu").click(function (event) { - $("#footer_super_menu").transMenu("open"); - }); - }, - - /**** - ***** - ****/ - - updateFreeSpaceInAddDialog: function () { - var formdir = $('input#add-dialog-folder-input').val(); - this.remote.getFreeSpace(formdir, this.onFreeSpaceResponse, this); - }, - - onFreeSpaceResponse: function (dir, bytes) { - var e, str, formdir; - - formdir = $('input#add-dialog-folder-input').val(); - if (formdir == dir) { - e = $('label#add-dialog-folder-label'); - if (bytes > 0) { - str = ' (' + Transmission.fmt.size(bytes) + ' Free)'; - } else { - str = ''; - }; - e.html('Destination folder' + str + ':'); - } - }, - - /**** - ***** - ***** UTILITIES - ***** - ****/ - - getAllTorrents: function () { - var torrents = []; - for (var key in this._torrents) { - torrents.push(this._torrents[key]); - }; - return torrents; - }, - - getTorrentIds: function (torrents) { - return $.map(torrents.slice(0), function (t) { - return t.getId(); - }); - }, - - scrollToRow: function (row) { - if (isMobileDevice) { - // FIXME: why? return - var list = $('#torrent_container'); - var scrollTop = list.scrollTop(); - var innerHeight = list.innerHeight(); - var offsetTop = row.getElement().offsetTop; - var offsetHeight = $(row.getElement()).outerHeight(); - - if (offsetTop < scrollTop) { - list.scrollTop(offsetTop); - } else if (innerHeight + scrollTop < offsetTop + offsetHeight) { - list.scrollTop(offsetTop + offsetHeight - innerHeight); - }; - }; - }, - - seedRatioLimit: function () { - var p = this.sessionProperties; - if (p && p.seedRatioLimited) { - return p.seedRatioLimit; - }; - return -1; - }, - - setPref: function (key, val) { - this[key] = val; - Prefs.setValue(key, val); - }, - - /**** - ***** - ***** SELECTION - ***** - ****/ - - getSelectedRows: function () { - return $.grep(this._rows, function (r) { - return r.isSelected(); - }); - }, - - getSelectedTorrents: function () { - return $.map(this.getSelectedRows(), function (r) { - return r.getTorrent(); - }); - }, - - getSelectedTorrentIds: function () { - return this.getTorrentIds(this.getSelectedTorrents()); - }, - - setSelectedRow: function (row) { - $(this.elements.torrent_list).children('.selected').removeClass('selected'); - this.selectRow(row); - }, - - selectRow: function (row) { - $(row.getElement()).addClass('selected'); - this.callSelectionChangedSoon(); - }, - - deselectRow: function (row) { - $(row.getElement()).removeClass('selected'); - this.callSelectionChangedSoon(); - }, - - selectAll: function () { - $(this.elements.torrent_list).children().addClass('selected'); - this.callSelectionChangedSoon(); - }, - deselectAll: function () { - $(this.elements.torrent_list).children('.selected').removeClass('selected'); - this.callSelectionChangedSoon(); - delete this._last_torrent_clicked; - }, - - indexOfLastTorrent: function () { - for (var i = 0, r; r = this._rows[i]; ++i) { - if (r.getTorrentId() === this._last_torrent_clicked) { - return i; - }; - }; - return -1; - }, - - // Select a range from this row to the last clicked torrent - selectRange: function (row) { - var last = this.indexOfLastTorrent(); - - if (last === -1) { - this.selectRow(row); - } else { // select the range between the prevous & current - var next = this._rows.indexOf(row); - var min = Math.min(last, next); - var max = Math.max(last, next); - for (var i = min; i <= max; ++i) { - this.selectRow(this._rows[i]); - }; - } - - this.callSelectionChangedSoon(); - }, - - selectionChanged: function () { - this.updateButtonStates(); - - this.inspector.setTorrents(this.inspectorIsVisible() ? this.getSelectedTorrents() : []); - - clearTimeout(this.selectionChangedTimer); - delete this.selectionChangedTimer; - - }, - - callSelectionChangedSoon: function () { - if (!this.selectionChangedTimer) { - var callback = $.proxy(this.selectionChanged, this), - msec = 200; - this.selectionChangedTimer = setTimeout(callback, msec); - } - }, - - /*-------------------------------------------- - * - * E V E N T F U N C T I O N S - * - *--------------------------------------------*/ - - /* - * Process key event - */ - keyDown: function (ev) { - var handled = false; - var rows = this._rows; - var isInputFocused = $(ev.target).is('input'); - var isDialogVisible = ($('.dialog_heading:visible').length > 0 || $('.ui-dialog:visible').length > 0); - - // hotkeys - var up_key = ev.keyCode === 38; // up key pressed - var dn_key = ev.keyCode === 40; // down key pressed - var a_key = ev.keyCode === 65; // a key pressed - var c_key = ev.keyCode === 67; // c key pressed - var d_key = ev.keyCode === 68; // d key pressed - var i_key = ev.keyCode === 73; // i key pressed - var l_key = ev.keyCode === 76; // l key pressed - var m_key = ev.keyCode === 77; // m key pressed - var o_key = ev.keyCode === 79; // o key pressed - var p_key = ev.keyCode === 80; // p key pressed - var r_key = ev.keyCode === 82; // r key pressed - var t_key = ev.keyCode === 84; // t key pressed - var u_key = ev.keyCode === 85; // u key pressed - var shift_key = ev.keyCode === 16; // shift key pressed - var slash_key = ev.keyCode === 191; // slash (/) key pressed - var backspace_key = ev.keyCode === 8; // backspace key pressed - var del_key = ev.keyCode === 46; // delete key pressed - var enter_key = ev.keyCode === 13; // enter key pressed - var esc_key = ev.keyCode === 27; // esc key pressed - var comma_key = ev.keyCode === 188; // comma key pressed - - if (enter_key) { - // handle other dialogs - if (dialog && dialog.isVisible()) { - dialog.executeCallback(); - handled = true; - } - - // handle upload dialog - if ($('#upload_container').is(':visible')) { - this.confirmUploadClicked(); - handled = true; - } - - // handle move dialog - if ($('#move_container').is(':visible')) { - this.confirmMoveClicked(); - handled = true; - } - - // handle rename dialog - if ($('#rename_container').is(':visible')) { - this.confirmRenameClicked(); - handled = true; - } - } - - if (esc_key) { - // handle other dialogs - if (dialog && dialog.isVisible()) { - dialog.hideDialog(); - handled = true; - } - - // handle upload dialog - if ($('#upload_container').is(':visible')) { - this.hideUploadDialog(); - handled = true; - } - - // handle move dialog - if ($('#move_container').is(':visible')) { - this.hideMoveDialog(); - handled = true; - } - - // handle rename dialog - if ($('#rename_container').is(':visible')) { - this.hideRenameDialog(); - handled = true; - } - } - - // Some hotkeys can only be used if the following conditions are met: - // 1. when no input fields are focused - // 2. when no other dialogs are visible - // 3. when the meta or ctrl key isn't pressed (i.e. opening dev tools shouldn't trigger the info panel) - if (!isInputFocused && !isDialogVisible && !ev.metaKey && !ev.ctrlKey) { - if (comma_key) { - this.togglePrefsDialogClicked(); - handled = true; - } - - if (slash_key) { - this.showHotkeysDialog(); - handled = true; - } - - if (a_key) { - if (ev.shiftKey) { - this.deselectAll(); - } else { - this.selectAll(); - } - handled = true; - } - - if (c_key) { - this.toggleCompactClicked(); - handled = true; - } - - if ((backspace_key || del_key || d_key) && rows.length) { - this.removeSelectedTorrents(); - handled = true; - } - - if (i_key) { - this.toggleInspector(); - handled = true; - } - - if (m_key || l_key) { - this.moveSelectedTorrents(); - handled = true; - } - - if (o_key || u_key) { - this.openTorrentClicked(ev); - handled = true; - } - - if (p_key) { - this.stopSelectedTorrents(); - handled = true; - } - - if (r_key) { - this.startSelectedTorrents(); - handled = true; - } - - if (t_key) { - this.toggleTurtleClicked(); - handled = true; - } - - if ((up_key || dn_key) && rows.length) { - var last = this.indexOfLastTorrent(), - i = last, - anchor = this._shift_index, - r, - min = 0, - max = rows.length - 1; - - if (dn_key && (i + 1 <= max)) { - ++i; - } else if (up_key && (i - 1 >= min)) { - --i; - }; - - var r = rows[i]; - - if (anchor >= 0) { - // user is extending the selection - // with the shift + arrow keys... - if (((anchor <= last) && (last < i)) || ((anchor >= last) && (last > i))) { - this.selectRow(r); - } else if (((anchor >= last) && (i > last)) || ((anchor <= last) && (last > i))) { - this.deselectRow(rows[last]); - } - } else { - if (ev.shiftKey) { - this.selectRange(r); - } else { - this.setSelectedRow(r); - }; - } - this._last_torrent_clicked = r.getTorrentId(); - this.scrollToRow(r); - handled = true; - } else if (shift_key) { - this._shift_index = this.indexOfLastTorrent(); - } - } - - return !handled; - }, - - keyUp: function (ev) { - if (ev.keyCode === 16) { // shift key pressed - delete this._shift_index; - }; - }, - - isButtonEnabled: function (ev) { - var p = (ev.target || ev.srcElement).parentNode; - return p.className !== 'disabled' && p.parentNode.className !== 'disabled'; - }, - - stopSelectedClicked: function (ev) { - if (this.isButtonEnabled(ev)) { - this.stopSelectedTorrents(); - this.hideMobileAddressbar(); - } - }, - - startSelectedClicked: function (ev) { - if (this.isButtonEnabled(ev)) { - this.startSelectedTorrents(false); - this.hideMobileAddressbar(); - } - }, - - stopAllClicked: function (ev) { - if (this.isButtonEnabled(ev)) { - this.stopAllTorrents(); - this.hideMobileAddressbar(); - } - }, - - startAllClicked: function (ev) { - if (this.isButtonEnabled(ev)) { - this.startAllTorrents(false); - this.hideMobileAddressbar(); - } - }, - - openTorrentClicked: function (ev) { - if (this.isButtonEnabled(ev)) { - $('body').addClass('open_showing'); - this.uploadTorrentFile(); - this.updateButtonStates(); - } - }, - - dragenter: function (ev) { - if (ev.dataTransfer && ev.dataTransfer.types) { - var types = ["text/uri-list", "text/plain"]; - for (var i = 0; i < types.length; ++i) { - // it would be better to look at the links here; - // sadly, with Firefox, trying would throw. - if (ev.dataTransfer.types.contains(types[i])) { - ev.stopPropagation(); - ev.preventDefault(); - ev.dropEffect = "copy"; - return false; - } - } - } else if (ev.dataTransfer) { - ev.dataTransfer.dropEffect = "none"; - } - return true; - }, - - drop: function (ev) { - var i, uri; - var uris = null; - var types = ["text/uri-list", "text/plain"]; - var paused = this.shouldAddedTorrentsStart(); - - if (!ev.dataTransfer || !ev.dataTransfer.types) { - return true; - }; - - for (i = 0; !uris && i < types.length; ++i) { - if (ev.dataTransfer.types.contains(types[i])) { - uris = ev.dataTransfer.getData(types[i]).split("\n"); - }; - }; - - for (i = 0; uri = uris[i]; ++i) { - if (/^#/.test(uri)) { // lines which start with "#" are comments - continue; - }; - if (/^[a-z-]+:/i.test(uri)) { // close enough to a url - this.remote.addTorrentByUrl(uri, paused); - }; - }; - - ev.preventDefault(); - return false; - }, - - hideUploadDialog: function () { - $('body.open_showing').removeClass('open_showing'); - $('#upload_container').hide(); - this.updateButtonStates(); - }, - - confirmUploadClicked: function () { - this.uploadTorrentFile(true); - this.hideUploadDialog(); - }, - - hideMoveDialog: function () { - $('#move_container').hide(); - this.updateButtonStates(); - }, - - confirmMoveClicked: function () { - this.moveSelectedTorrents(true); - this.hideUploadDialog(); - }, - - hideRenameDialog: function () { - $('body.open_showing').removeClass('open_showing'); - $('#rename_container').hide(); - }, - - confirmRenameClicked: function () { - var torrents = this.getSelectedTorrents(); - this.renameTorrent(torrents[0], $('input#torrent_rename_name').attr('value')); - this.hideRenameDialog(); - }, - - removeClicked: function (ev) { - if (this.isButtonEnabled(ev)) { - this.removeSelectedTorrents(); - this.hideMobileAddressbar(); - }; - }, - - // turn the periodic ajax session refresh on & off - togglePeriodicSessionRefresh: function (enabled) { - var that = this, - msec = 8000; - - function callback() { - that.loadDaemonPrefs(undefined, rescheduleTimeout); - } - - function rescheduleTimeout() { - that.sessionTimeout = setTimeout(callback, msec); - } - - clearTimeout(this.sessionTimeout); - delete this.sessionTimeout; - - if (enabled) { - rescheduleTimeout(); - } - }, - - toggleTurtleClicked: function () { - var o = {}; - o[RPC._TurtleState] = !$('#turtle-button').hasClass('selected'); - this.remote.savePrefs(o); - }, - - /*-------------------------------------------- - * - * I N T E R F A C E F U N C T I O N S - * - *--------------------------------------------*/ - - onPrefsDialogClosed: function () { - $('#prefs-button').removeClass('selected'); - }, - - togglePrefsDialogClicked: function (ev) { - var e = $('#prefs-button'); - - if (e.hasClass('selected')) - this.prefsDialog.close(); - else { - e.addClass('selected'); - this.prefsDialog.show(); - } - }, - - setFilterText: function (search) { - this.filterText = search ? search.trim() : null; - this.refilter(true); - }, - - setSortMethod: function (sort_method) { - this.setPref(Prefs._SortMethod, sort_method); - this.refilter(true); - }, - - setSortDirection: function (direction) { - this.setPref(Prefs._SortDirection, direction); - this.refilter(true); - }, - - onMenuClicked: function (event, ui) { - var o, dir; - var id = ui.id; - var remote = this.remote; - var element = ui.target; - - if (ui.group == 'sort-mode') { - element.selectMenuItem(); - this.setSortMethod(id.replace(/sort_by_/, '')); - } else if (element.hasClass('upload-speed')) { - o = {}; - o[RPC._UpSpeedLimit] = parseInt(element.text()); - o[RPC._UpSpeedLimited] = true; - remote.savePrefs(o); - } else if (element.hasClass('download-speed')) { - o = {}; - o[RPC._DownSpeedLimit] = parseInt(element.text()); - o[RPC._DownSpeedLimited] = true; - remote.savePrefs(o); - } else { - switch (id) { - case 'statistics': - this.showStatsDialog(); - break; - - case 'hotkeys': - this.showHotkeysDialog(); - break; - - case 'about-button': - o = 'Transmission ' + this.serverVersion; - $('#about-dialog #about-title').html(o); - $('#about-dialog').dialog({ - title: 'About', - show: 'fade', - hide: 'fade' - }); - break; - - case 'homepage': - window.open('https://transmissionbt.com/'); - break; - - case 'tipjar': - window.open('https://transmissionbt.com/donate/'); - break; - - case 'unlimited_download_rate': - o = {}; - o[RPC._DownSpeedLimited] = false; - remote.savePrefs(o); - break; - - case 'limited_download_rate': - o = {}; - o[RPC._DownSpeedLimited] = true; - remote.savePrefs(o); - break; - - case 'unlimited_upload_rate': - o = {}; - o[RPC._UpSpeedLimited] = false; - remote.savePrefs(o); - break; - - case 'limited_upload_rate': - o = {}; - o[RPC._UpSpeedLimited] = true; - remote.savePrefs(o); - break; - - case 'reverse_sort_order': - if (element.menuItemIsSelected()) { - dir = Prefs._SortAscending; - element.deselectMenuItem(); - } else { - dir = Prefs._SortDescending; - element.selectMenuItem(); - } - this.setSortDirection(dir); - break; - - case 'toggle_notifications': - Notifications && Notifications.toggle(); - break; - - default: - console.log('unhandled: ' + id); - break; - }; - }; - }, - - onTorrentChanged: function (ev, tor) { - // update our dirty fields - this.dirtyTorrents[tor.getId()] = true; - - // enqueue ui refreshes - this.refilterSoon(); - this.updateButtonsSoon(); - }, - - updateFromTorrentGet: function (updates, removed_ids) { - var i, o, t, id, needed, callback, fields; - var needinfo = []; - - for (i = 0; o = updates[i]; ++i) { - id = o.id; - if ((t = this._torrents[id])) { - needed = t.needsMetaData(); - t.refresh(o); - if (needed && !t.needsMetaData()) { - needinfo.push(id); - }; - } else { - t = this._torrents[id] = new Torrent(o); - this.dirtyTorrents[id] = true; - callback = $.proxy(this.onTorrentChanged, this); - $(t).bind('dataChanged', callback); - // do we need more info for this torrent? - if (!('name' in t.fields) || !('status' in t.fields)) - needinfo.push(id); - - t.notifyOnFieldChange('status', $.proxy(function (newValue, oldValue) { - if (oldValue === Torrent._StatusDownload && (newValue == Torrent._StatusSeed || newValue == Torrent._StatusSeedWait)) { - $(this).trigger('downloadComplete', [t]); - } else if (oldValue === Torrent._StatusSeed && newValue === Torrent._StatusStopped && t.isFinished()) { - $(this).trigger('seedingComplete', [t]); - } else { - $(this).trigger('statusChange', [t]); - } - }, this)); - } - } - - if (needinfo.length) { - // whee, new torrents! get their initial information. - fields = ['id'].concat(Torrent.Fields.Metadata, - Torrent.Fields.Stats); - this.updateTorrents(needinfo, fields); - this.refilterSoon(); - } - - if (removed_ids) { - this.deleteTorrents(removed_ids); - this.refilterSoon(); - } - }, - - updateTorrents: function (ids, fields, callback) { - var that = this; - - function f(updates, removedIds) { - if (callback) { - callback(); - } - - that.updateFromTorrentGet(updates, removedIds); - } - - this.remote.updateTorrents(ids, fields, f); - }, - - refreshTorrents: function () { - var callback = $.proxy(this.refreshTorrents, this); - var msec = this[Prefs._RefreshRate] * 1000; - var fields = ['id'].concat(Torrent.Fields.Stats); - - // send a request right now - this.updateTorrents('recently-active', fields); - - // schedule the next request - clearTimeout(this.refreshTorrentsTimeout); - this.refreshTorrentsTimeout = setTimeout(callback, msec); - }, - - initializeTorrents: function () { - var fields = ['id'].concat(Torrent.Fields.Metadata, Torrent.Fields.Stats); - this.updateTorrents(null, fields); - }, - - onRowClicked: function (ev) { - var meta_key = ev.metaKey || ev.ctrlKey, - row = ev.currentTarget.row; - - // handle the per-row "torrent_resume" button - if (ev.target.className === 'torrent_resume') { - this.startTorrent(row.getTorrent()); - return; - } - - // handle the per-row "torrent_pause" button - if (ev.target.className === 'torrent_pause') { - this.stopTorrent(row.getTorrent()); - return; - } - - // Prevents click carrying to parent element - // which deselects all on click - ev.stopPropagation(); - - if (isMobileDevice) { - if (row.isSelected()) - this.setInspectorVisible(true); - this.setSelectedRow(row); - - } else if (ev.shiftKey) { - this.selectRange(row); - // Need to deselect any selected text - window.focus(); - - // Apple-Click, not selected - } else if (!row.isSelected() && meta_key) { - this.selectRow(row); - - // Regular Click, not selected - } else if (!row.isSelected()) { - this.setSelectedRow(row); - - // Apple-Click, selected - } else if (row.isSelected() && meta_key) { - this.deselectRow(row); - - // Regular Click, selected - } else if (row.isSelected()) { - this.setSelectedRow(row); - } - - this._last_torrent_clicked = row.getTorrentId(); - }, - - deleteTorrents: function (ids) { - var i, id; - - if (ids && ids.length) { - for (i = 0; id = ids[i]; ++i) { - this.dirtyTorrents[id] = true; - delete this._torrents[id]; - }; - this.refilter(); - }; - }, - - shouldAddedTorrentsStart: function () { - return this.prefsDialog.shouldAddedTorrentsStart(); - }, - - /* - * Select a torrent file to upload - */ - uploadTorrentFile: function (confirmed) { - var fileInput = $('input#torrent_upload_file'); - var folderInput = $('input#add-dialog-folder-input'); - var startInput = $('input#torrent_auto_start'); - var urlInput = $('input#torrent_upload_url'); - - if (!confirmed) { - // update the upload dialog's fields - fileInput.attr('value', ''); - urlInput.attr('value', ''); - startInput.attr('checked', this.shouldAddedTorrentsStart()); - folderInput.attr('value', $("#download-dir").val()); - folderInput.change($.proxy(this.updateFreeSpaceInAddDialog, this)); - this.updateFreeSpaceInAddDialog(); - - // show the dialog - $('#upload_container').show(); - urlInput.focus(); - } else { - var paused = !startInput.is(':checked'); - var destination = folderInput.val(); - var remote = this.remote; - - jQuery.each(fileInput[0].files, function (i, file) { - var reader = new FileReader(); - reader.onload = function (e) { - var contents = e.target.result; - var key = "base64," - var index = contents.indexOf(key); - if (index > -1) { - var metainfo = contents.substring(index + key.length); - var o = { - method: 'torrent-add', - arguments: { - 'paused': paused, - 'download-dir': destination, - 'metainfo': metainfo - } - }; - remote.sendRequest(o, function (response) { - if (response.result != 'success') - alert('Error adding "' + file.name + '": ' + response.result); - }); - } - }; - reader.readAsDataURL(file); - }); - - var url = $('#torrent_upload_url').val(); - if (url != '') { - if (url.match(/^[0-9a-f]{40}$/i)) { - url = 'magnet:?xt=urn:btih:' + url; - }; - var o = { - 'method': 'torrent-add', - arguments: { - 'paused': paused, - 'download-dir': destination, - 'filename': url - } - }; - remote.sendRequest(o, function (response) { - if (response.result != 'success') { - alert('Error adding "' + url + '": ' + response.result); - }; - }); - } - } - }, - - promptSetLocation: function (confirmed, torrents) { - if (!confirmed) { - var path; - if (torrents.length === 1) { - path = torrents[0].getDownloadDir(); - } else { - path = $("#download-dir").val(); - } - $('input#torrent_path').attr('value', path); - $('#move_container').show(); - $('#torrent_path').focus(); - } else { - var ids = this.getTorrentIds(torrents); - this.remote.moveTorrents(ids, $("input#torrent_path").val(), this.refreshTorrents, this); - $('#move_container').hide(); - } - }, - - moveSelectedTorrents: function (confirmed) { - var torrents = this.getSelectedTorrents(); - if (torrents.length) { - this.promptSetLocation(confirmed, torrents); - }; - }, - - removeSelectedTorrents: function () { - var torrents = this.getSelectedTorrents(); - if (torrents.length) { - this.promptToRemoveTorrents(torrents); - }; - }, - - removeSelectedTorrentsAndData: function () { - var torrents = this.getSelectedTorrents(); - if (torrents.length) { - this.promptToRemoveTorrentsAndData(torrents); - }; - }, - - promptToRemoveTorrents: function (torrents) { - if (torrents.length === 1) { - var torrent = torrents[0]; - var header = 'Remove ' + torrent.getName() + '?'; - var message = 'Once removed, continuing the transfer will require the torrent file. Are you sure you want to remove it?'; - - dialog.confirm(header, message, 'Remove', function () { - transmission.removeTorrents(torrents); - }); - } else { - var header = 'Remove ' + torrents.length + ' transfers?'; - var message = 'Once removed, continuing the transfers will require the torrent files. Are you sure you want to remove them?'; - - dialog.confirm(header, message, 'Remove', function () { - transmission.removeTorrents(torrents); - }); - } - }, - - promptToRemoveTorrentsAndData: function (torrents) { - if (torrents.length === 1) { - var torrent = torrents[0]; - var header = 'Remove ' + torrent.getName() + ' and delete data?'; - var message = 'All data downloaded for this torrent will be deleted. Are you sure you want to remove it?'; - - dialog.confirm(header, message, 'Remove', function () { - transmission.removeTorrentsAndData(torrents); - }); - } else { - var header = 'Remove ' + torrents.length + ' transfers and delete data?'; - var message = 'All data downloaded for these torrents will be deleted. Are you sure you want to remove them?'; - - dialog.confirm(header, message, 'Remove', function () { - transmission.removeTorrentsAndData(torrents); - }); - } - }, - - removeTorrents: function (torrents) { - var ids = this.getTorrentIds(torrents); - this.remote.removeTorrents(ids, this.refreshTorrents, this); - }, - - removeTorrentsAndData: function (torrents) { - this.remote.removeTorrentsAndData(torrents); - }, - - promptToRenameTorrent: function (torrent) { - $('body').addClass('open_showing'); - $('input#torrent_rename_name').attr('value', torrent.getName()); - $('#rename_container').show(); - $('#torrent_rename_name').focus(); - }, - - renameSelectedTorrents: function () { - var torrents = this.getSelectedTorrents(); - if (torrents.length != 1) { - dialog.alert("Renaming", "You can rename only one torrent at a time.", "Ok"); - } else { - this.promptToRenameTorrent(torrents[0]); - }; - }, - - onTorrentRenamed: function (response) { - var torrent; - if ((response.result === 'success') && (response.arguments) && ((torrent = this._torrents[response.arguments.id]))) { - torrent.refresh(response.arguments); - } - }, - - renameTorrent: function (torrent, newname) { - var oldpath = torrent.getName(); - this.remote.renameTorrent([torrent.getId()], oldpath, newname, this.onTorrentRenamed, this); - }, - - verifySelectedTorrents: function () { - this.verifyTorrents(this.getSelectedTorrents()); - }, - - reannounceSelectedTorrents: function () { - this.reannounceTorrents(this.getSelectedTorrents()); - }, - - startAllTorrents: function (force) { - this.startTorrents(this.getAllTorrents(), force); - }, - startSelectedTorrents: function (force) { - this.startTorrents(this.getSelectedTorrents(), force); - }, - startTorrent: function (torrent) { - this.startTorrents([torrent], false); - }, - - startTorrents: function (torrents, force) { - this.remote.startTorrents(this.getTorrentIds(torrents), force, this.refreshTorrents, this); - }, - verifyTorrent: function (torrent) { - this.verifyTorrents([torrent]); - }, - verifyTorrents: function (torrents) { - this.remote.verifyTorrents(this.getTorrentIds(torrents), this.refreshTorrents, this); - }, - - reannounceTorrent: function (torrent) { - this.reannounceTorrents([torrent]); - }, - reannounceTorrents: function (torrents) { - this.remote.reannounceTorrents(this.getTorrentIds(torrents), this.refreshTorrents, this); - }, - - stopAllTorrents: function () { - this.stopTorrents(this.getAllTorrents()); - }, - stopSelectedTorrents: function () { - this.stopTorrents(this.getSelectedTorrents()); - }, - stopTorrent: function (torrent) { - this.stopTorrents([torrent]); - }, - stopTorrents: function (torrents) { - this.remote.stopTorrents(this.getTorrentIds(torrents), this.refreshTorrents, this); - }, - changeFileCommand: function (torrentId, rowIndices, command) { - this.remote.changeFileCommand(torrentId, rowIndices, command); - }, - - hideMobileAddressbar: function (delaySecs) { - if (isMobileDevice && !scroll_timeout) { - var callback = $.proxy(this.doToolbarHide, this); - var msec = delaySecs * 1000 || 150; - scroll_timeout = setTimeout(callback, msec); - }; - }, - doToolbarHide: function () { - window.scrollTo(0, 1); - scroll_timeout = null; - }, - - // Queue - moveTop: function () { - this.remote.moveTorrentsToTop(this.getSelectedTorrentIds(), this.refreshTorrents, this); - }, - moveUp: function () { - this.remote.moveTorrentsUp(this.getSelectedTorrentIds(), this.refreshTorrents, this); - }, - moveDown: function () { - this.remote.moveTorrentsDown(this.getSelectedTorrentIds(), this.refreshTorrents, this); - }, - moveBottom: function () { - this.remote.moveTorrentsToBottom(this.getSelectedTorrentIds(), this.refreshTorrents, this); - }, - - /*** - **** - ***/ - - updateGuiFromSession: function (o) { - var limit, limited, e, b, text; - var fmt = Transmission.fmt; - var menu = $('#footer_super_menu'); - - this.serverVersion = o.version; - - this.prefsDialog.set(o); - - if (RPC._TurtleState in o) { - b = o[RPC._TurtleState]; - e = $('#turtle-button'); - text = ['Click to ', (b ? 'disable' : 'enable'), ' Temporary Speed Limits (', fmt.speed(o[RPC._TurtleUpSpeedLimit]), ' up,', fmt.speed(o[RPC._TurtleDownSpeedLimit]), ' down)'].join(''); - e.toggleClass('selected', b); - e.attr('title', text); - } - - if (this.isMenuEnabled && (RPC._DownSpeedLimited in o) && (RPC._DownSpeedLimit in o)) { - limit = o[RPC._DownSpeedLimit]; - limited = o[RPC._DownSpeedLimited]; - - e = menu.find('#limited_download_rate'); - e.html('Limit (' + fmt.speed(limit) + ')'); - - if (!limited) { - e = menu.find('#unlimited_download_rate'); - }; - e.selectMenuItem(); - } - - if (this.isMenuEnabled && (RPC._UpSpeedLimited in o) && (RPC._UpSpeedLimit in o)) { - limit = o[RPC._UpSpeedLimit]; - limited = o[RPC._UpSpeedLimited]; - - e = menu.find('#limited_upload_rate'); - e.html('Limit (' + fmt.speed(limit) + ')'); - - if (!limited) { - e = menu.find('#unlimited_upload_rate'); - }; - e.selectMenuItem(); - } - }, - - updateStatusbar: function () { - var i, row; - var u = 0; - var d = 0; - var fmt = Transmission.fmt; - var torrents = this.getAllTorrents(); - - // up/down speed - for (i = 0; row = torrents[i]; ++i) { - u += row.getUploadSpeed(); - d += row.getDownloadSpeed(); - } - - $('#speed-up-container').toggleClass('active', u > 0); - $('#speed-up-label').text(fmt.speedBps(u)); - - $('#speed-dn-container').toggleClass('active', d > 0); - $('#speed-dn-label').text(fmt.speedBps(d)); - - // visible torrents - $('#filter-count').text(fmt.countString('Transfer', 'Transfers', this._rows.length)); - }, - - setEnabled: function (key, flag) { - $(key).toggleClass('disabled', !flag); - }, - - updateFilterSelect: function () { - var i, names, name, str, o; - var trackers = this.getTrackers(); - - // build a sorted list of names - names = []; - for (name in trackers) { - names.push(name); - }; - names.sort(); - - // build the new html - if (!this.filterTracker) { - str = ''; - } else { - str = ''; - }; - for (i = 0; name = names[i]; ++i) { - o = trackers[name]; - str += ''; + } else { + str = ''; + } + for (i = 0; (name = names[i]); ++i) { + o = trackers[name]; + str += '