Squashed commit of the following:
commit4b9b9920bfAuthor: Charles Kerr <charles@charleskerr.com> Date: Mon Jun 19 16:51:34 2023 -0500 refactor: prefer std::map over QMap in transmission-qt (#5641) * refactor: use std::map instead of QMap in PrefsDialog.cc * refactor: use std::map instead of QMap in DetailsDialog.cc * refactor: use std::map instead of QMap in OptionsDialog.cc * refactor: use std::map instead of QMap in FileTreeModel.cc commit040bc8a1ceAuthor: Dmitry Antipov <dmantipov@yandex.ru> Date: Tue Jun 20 00:22:54 2023 +0300 fix: Qt 6.5 deprecation warning (#5552) commit23a52fa1c5Author: LaserEyess <16581533+LaserEyess@users.noreply.github.com> Date: Mon Jun 19 16:51:20 2023 -0400 fixup: dedup tr_rpc_address with tr_address (#5523) * fixup: dedup tr_rpc_address with tr_address tr_rpc_address was duplicating a lot of work that tr_address was doing a lot better. Fall back to using tr_address for ipv4/ipv6 connections and make a tr_unix_addr for handling unix sockets. * remove unnecessary functions commitddac05954bAuthor: Василий Чай <basilefff@gmail.com> Date: Mon Jun 19 09:30:55 2023 +0400 fix: return error when renaming into existing file (#5563) commitb8ff35c4ceAuthor: tearfur <46261767+tearfur@users.noreply.github.com> Date: Mon Jun 19 08:06:31 2023 +0800 handle IPv6 NAT during LTEP handshake (#5565) * fix: peer handshake reported wrong ipv6 address https://github.com/transmission/transmission/issues/5542#issuecomment-1556710922 commitfd583ac878Author: Charles Kerr <charles@charleskerr.com> Date: Sun Jun 18 17:36:39 2023 -0500 deps: bump libfmt to v10.0.0 (#5635) seems to be semver/minor safe for our API use Fixes #5511. Possibly fixes #5627. commit1664088ba5Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun Jun 18 15:55:43 2023 -0500 chore: update generated transmission-web files (#5588) commit0fd7989b18Author: Gary Elshaw <69029666+GaryElshaw@users.noreply.github.com> Date: Mon Jun 19 07:11:56 2023 +1200 fix: restore png files that were corrupted in recent "compression" script commitbd9d110d45Author: Cœur <coeur@gmx.fr> Date: Sun Jun 18 20:32:34 2023 +0200 fix 'setNeedsDisplay' is deprecated: first deprecated in macOS 10.14 (#5633) commit76166d8fa7Author: Cœur <coeur@gmx.fr> Date: Mon Jun 12 18:03:22 2023 +0200 refactor: replace NSMutableDictionary with constant attributes (#5221) commitc379cd727fAuthor: Robin Seth Ekman <robin.seth.ekman@gmail.com> Date: Mon Jun 12 03:19:54 2023 +0200 fix: transmission-remote only list every other label (fixes #5571) (#5572) commit87f254ae90Author: Charles Kerr <charles@charleskerr.com> Date: Sun Jun 11 18:52:45 2023 -0500 chore: bump fast_float snaapshot to 5.2.0 (#5605) commit802619e174Author: tearfur <46261767+tearfur@users.noreply.github.com> Date: Mon Jun 12 07:27:01 2023 +0800 fix: fixes and improvements to global IP query (#5510) commitc8e84f870bAuthor: Charles Kerr <charles@charleskerr.com> Date: Sun Jun 11 16:28:43 2023 -0500 ci: remove "brew update" step (#5606) commit60c68afddeAuthor: Charles Kerr <charles@charleskerr.com> Date: Sun Jun 11 11:25:35 2023 -0500 Revert "fix: some labels not displayed in transmission-remote (#5600)" This reverts commitebd5080a93. commitebd5080a93Author: Charles Kerr <charles@charleskerr.com> Date: Sat Jun 10 19:01:32 2023 -0500 fix: some labels not displayed in transmission-remote (#5600) Fixes #5571. commit8ca02b8f28Author: Mike Gelfand <mikedld@users.noreply.github.com> Date: Sun Jun 11 02:09:18 2023 +0400 Avoid locale use in `tr_truncd()` (#5587) `tr_parseNum<>()` is implemented with `fast_float::from_chars()`, and the latter is documented as "expecting a locale-indepent format equivalent to what is used by std::strtod in the default ("C") locale". To accomodate locale independent number parsing, switch back to locale-independent number formatting in `tr_truncd()` by both removing a `L` format specifier from `fmt::format_to_n()` call and using a fixed "." decimal separator when truncating the value. commit0ef58c2a20Author: Charles Kerr <charles@charleskerr.com> Date: Mon Jun 5 17:03:11 2023 -0500 chore: improve lossless compression of png files (#5586) commitc1c27f3da0Author: Daniel Kamil Kozar <dkk089@gmail.com> Date: Mon Jun 5 20:15:32 2023 +0200 Expose files' begin and end pieces via RPC (#5578) * Expose files' begin and end pieces via RPC This adds two arguments, `beginPiece` and `endPiece`, for each of the entries in the `files` array of the RPC's `torrent-`get` method. The point is to allow RPC clients to display a file's completion progress as piece-based in addition to byte-based. commit61679e1adcAuthor: Cœur <coeur@gmx.fr> Date: Mon May 29 15:43:43 2023 +0200 Support localized punctuation for "Port:" (#4452) * Support localized punctuation for "Port:" * Code review: adopting "Port: %@" * updating other locales for "Port" -> "Port: %@"
2
.github/workflows/actions.yml
vendored
@@ -239,7 +239,6 @@ jobs:
|
||||
sw_vers
|
||||
- name: Get Dependencies
|
||||
run: |
|
||||
brew update
|
||||
brew install cmake gettext libdeflate libevent libpsl miniupnpc ninja node pkg-config
|
||||
- name: Get Dependencies (GTK)
|
||||
if: ${{ needs.what-to-make.outputs.make-gtk == 'true' }}
|
||||
@@ -513,7 +512,6 @@ jobs:
|
||||
sw_vers
|
||||
- name: Get Dependencies
|
||||
run: |
|
||||
brew update
|
||||
brew install cmake gettext ninja node pkg-config
|
||||
- name: Get Dependencies (GTK)
|
||||
if: ${{ needs.what-to-make.outputs.make-gtk == 'true' }}
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 38 KiB |
@@ -294,6 +294,8 @@ The 'source' column here corresponds to the data structure there.
|
||||
| `bytesCompleted` | number | tr_file_view
|
||||
| `length` | number | tr_file_view
|
||||
| `name` | string | tr_file_view
|
||||
| `beginPiece` | number | tr_file_view
|
||||
| `endPiece` | number | tr_file_view
|
||||
|
||||
|
||||
`fileStats`: a file's non-constant properties. An array of `tr_info.filecount` objects, each containing:
|
||||
@@ -1009,3 +1011,5 @@ Transmission 4.1.0 (`rpc-version-semver` 5.4.0, `rpc-version`: 18)
|
||||
|:---|:---
|
||||
| `torrent-get` | new arg `sequentialDownload`
|
||||
| `torrent-set` | new arg `sequentialDownload`
|
||||
| `torrent-get` | new arg `files.beginPiece`
|
||||
| `torrent-get` | new arg `files.endPiece`
|
||||
|
||||
@@ -395,7 +395,7 @@ void register_magnet_link_handler()
|
||||
_("Couldn't register Transmission as a {content_type} handler: {error} ({error_code})"),
|
||||
fmt::arg("content_type", content_type),
|
||||
fmt::arg("error", e.what()),
|
||||
fmt::arg("error_code", e.code())));
|
||||
fmt::arg("error_code", static_cast<int>(e.code()))));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
icon.png
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -556,7 +556,7 @@ tr_sys_file_t tr_sys_file_get_std(tr_std_sys_file_t std_file, tr_error** error)
|
||||
break;
|
||||
|
||||
default:
|
||||
TR_ASSERT_MSG(false, fmt::format(FMT_STRING("unknown standard file {:d}"), std_file));
|
||||
TR_ASSERT_MSG(false, fmt::format(FMT_STRING("unknown standard file {:d}"), static_cast<int>(std_file)));
|
||||
tr_error_set_from_errno(error, EINVAL);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,25 @@ namespace
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
auto constexpr IPQueryServices = std::array{ "https://icanhazip.com"sv, "https://api64.ipify.org"sv };
|
||||
static_assert(TR_AF_INET == 0);
|
||||
static_assert(TR_AF_INET6 == 1);
|
||||
|
||||
auto constexpr protocol_str(tr_address_type type) noexcept
|
||||
{
|
||||
/* TODO: very slight performance nit
|
||||
* - If upgrading to C++20, change Map to a consteval lambda:
|
||||
* auto map = []() consteval { return std::array{ "IPv4"sv, "IPv6"sv }; };
|
||||
* - If upgrading to C++23, change Map to static constexpr
|
||||
*
|
||||
* Ref: https://wg21.link/p2647r1
|
||||
*/
|
||||
auto constexpr Map = std::array{ "IPv4"sv, "IPv6"sv };
|
||||
return Map[type];
|
||||
}
|
||||
|
||||
auto constexpr IPQueryServices = std::array{ std::array{ "https://ip4.transmissionbt.com/"sv },
|
||||
std::array{ "https://ip6.transmissionbt.com/"sv } };
|
||||
|
||||
auto constexpr UpkeepInterval = 30min;
|
||||
auto constexpr RetryUpkeepInterval = 30s;
|
||||
|
||||
@@ -36,11 +54,11 @@ auto constexpr RetryUpkeepInterval = 30s;
|
||||
|
||||
namespace
|
||||
{
|
||||
// Functions contained in external_source_ip_helpers are modified from code
|
||||
// Functions contained in global_source_ip_helpers are modified from code
|
||||
// by Juliusz Chroboczek and is covered under the same license as dht.cc.
|
||||
// Please feel free to copy them into your software if it can help
|
||||
// unbreaking the double-stack Internet.
|
||||
namespace external_source_ip_helpers
|
||||
namespace global_source_ip_helpers
|
||||
{
|
||||
|
||||
// Get the source address used for a given destination address.
|
||||
@@ -109,12 +127,12 @@ namespace external_source_ip_helpers
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace external_source_ip_helpers
|
||||
} // namespace global_source_ip_helpers
|
||||
} // namespace
|
||||
|
||||
tr_global_ip_cache::tr_global_ip_cache(tr_web& web_in, libtransmission::TimerMaker& timer_maker_in)
|
||||
: web_{ web_in }
|
||||
, upkeep_timers_{ timer_maker_in.create(), timer_maker_in.create() }
|
||||
tr_global_ip_cache::tr_global_ip_cache(Mediator& mediator_in)
|
||||
: mediator_{ mediator_in }
|
||||
, upkeep_timers_{ mediator_in.timer_maker().create(), mediator_in.timer_maker().create() }
|
||||
{
|
||||
static_assert(TR_AF_INET == 0);
|
||||
static_assert(TR_AF_INET6 == 1);
|
||||
@@ -132,6 +150,11 @@ tr_global_ip_cache::tr_global_ip_cache(tr_web& web_in, libtransmission::TimerMak
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<tr_global_ip_cache> tr_global_ip_cache::create(tr_global_ip_cache::Mediator& mediator_in)
|
||||
{
|
||||
return std::unique_ptr<tr_global_ip_cache>(new tr_global_ip_cache(mediator_in));
|
||||
}
|
||||
|
||||
tr_global_ip_cache::~tr_global_ip_cache()
|
||||
{
|
||||
// Destroying mutex while someone owns it is undefined behaviour, so we acquire it first
|
||||
@@ -164,37 +187,21 @@ bool tr_global_ip_cache::try_shutdown() noexcept
|
||||
return true;
|
||||
}
|
||||
|
||||
void tr_global_ip_cache::set_settings_bind_addr(tr_address_type type, std::string_view bind_address) noexcept
|
||||
{
|
||||
settings_bind_addr_[type] = tr_address::from_string(bind_address);
|
||||
if (settings_bind_addr_[type] && type != settings_bind_addr_[type]->type)
|
||||
{
|
||||
settings_bind_addr_[type].reset();
|
||||
}
|
||||
update_addr(type);
|
||||
}
|
||||
|
||||
tr_address tr_global_ip_cache::bind_addr(tr_address_type type) const noexcept
|
||||
{
|
||||
if (type == TR_AF_INET || type == TR_AF_INET6)
|
||||
{
|
||||
return settings_bind_addr_[type].value_or(type == TR_AF_INET ? tr_address::any_ipv4() : tr_address::any_ipv6());
|
||||
if (auto const addr = tr_address::from_string(mediator_.settings_bind_addr(type)); addr && type == addr->type)
|
||||
{
|
||||
return *addr;
|
||||
}
|
||||
return type == TR_AF_INET ? tr_address::any_ipv4() : tr_address::any_ipv6();
|
||||
}
|
||||
|
||||
TR_ASSERT_MSG(false, "invalid type");
|
||||
return {};
|
||||
}
|
||||
|
||||
void tr_global_ip_cache::update_addr(tr_address_type type) noexcept
|
||||
{
|
||||
update_source_addr(type);
|
||||
/* TODO: Temporarily disable because there is currently no way for this to work without using third party services */
|
||||
// if (global_source_addr(type))
|
||||
// {
|
||||
// update_global_addr(type);
|
||||
// }
|
||||
}
|
||||
|
||||
bool tr_global_ip_cache::set_global_addr(tr_address_type type, tr_address const& addr) noexcept
|
||||
{
|
||||
if (type == addr.type && addr.is_global_unicast_address())
|
||||
@@ -207,11 +214,125 @@ bool tr_global_ip_cache::set_global_addr(tr_address_type type, tr_address const&
|
||||
return false;
|
||||
}
|
||||
|
||||
void tr_global_ip_cache::update_addr(tr_address_type type) noexcept
|
||||
{
|
||||
update_source_addr(type);
|
||||
if (global_source_addr(type))
|
||||
{
|
||||
update_global_addr(type);
|
||||
}
|
||||
}
|
||||
|
||||
void tr_global_ip_cache::update_global_addr(tr_address_type type) noexcept
|
||||
{
|
||||
TR_ASSERT(has_ip_protocol_[type]);
|
||||
TR_ASSERT(ix_service_[type] < std::size(IPQueryServices[type]));
|
||||
|
||||
if (ix_service_[type] == 0U && !set_is_updating(type))
|
||||
{
|
||||
return;
|
||||
}
|
||||
TR_ASSERT(is_updating_[type] == is_updating_t::YES);
|
||||
|
||||
// Update global address
|
||||
auto options = tr_web::FetchOptions{ IPQueryServices[type][ix_service_[type]],
|
||||
[this, type](tr_web::FetchResponse const& response)
|
||||
{ this->on_response_ip_query(type, response); },
|
||||
nullptr };
|
||||
options.ip_proto = type == TR_AF_INET ? tr_web::FetchOptions::IPProtocol::V4 : tr_web::FetchOptions::IPProtocol::V6;
|
||||
options.sndbuf = 4096;
|
||||
options.rcvbuf = 4096;
|
||||
mediator_.fetch(std::move(options));
|
||||
}
|
||||
|
||||
void tr_global_ip_cache::update_source_addr(tr_address_type type) noexcept
|
||||
{
|
||||
using namespace global_source_ip_helpers;
|
||||
|
||||
TR_ASSERT(has_ip_protocol_[type]);
|
||||
|
||||
if (!set_is_updating(type))
|
||||
{
|
||||
return;
|
||||
}
|
||||
TR_ASSERT(is_updating_[type] == is_updating_t::YES);
|
||||
|
||||
auto const protocol = protocol_str(type);
|
||||
|
||||
auto err = int{ 0 };
|
||||
auto const& source_addr = get_global_source_address(bind_addr(type), err);
|
||||
if (source_addr)
|
||||
{
|
||||
set_source_addr(*source_addr);
|
||||
tr_logAddInfo(fmt::format(
|
||||
_("Successfully updated source {protocol} address to {ip}"),
|
||||
fmt::arg("protocol", protocol),
|
||||
fmt::arg("ip", source_addr->display_name())));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stop the update process since we have no public internet connectivity
|
||||
unset_addr(type);
|
||||
upkeep_timers_[type]->set_interval(RetryUpkeepInterval);
|
||||
tr_logAddDebug(fmt::format("Couldn't obtain source {} address", protocol));
|
||||
if (err == EAFNOSUPPORT)
|
||||
{
|
||||
stop_timer(type); // No point in retrying
|
||||
has_ip_protocol_[type] = false;
|
||||
tr_logAddInfo(fmt::format(_("Your machine does not support {protocol}"), fmt::arg("protocol", protocol)));
|
||||
}
|
||||
}
|
||||
|
||||
unset_is_updating(type);
|
||||
}
|
||||
|
||||
void tr_global_ip_cache::on_response_ip_query(tr_address_type type, tr_web::FetchResponse const& response) noexcept
|
||||
{
|
||||
TR_ASSERT(is_updating_[type] == is_updating_t::YES);
|
||||
TR_ASSERT(ix_service_[type] < std::size(IPQueryServices[type]));
|
||||
|
||||
auto const protocol = protocol_str(type);
|
||||
auto success = false;
|
||||
|
||||
if (response.status == 200 /* HTTP_OK */)
|
||||
{
|
||||
// Update member
|
||||
if (auto addr = tr_address::from_string(tr_strvStrip(response.body)); addr && set_global_addr(type, *addr))
|
||||
{
|
||||
success = true;
|
||||
upkeep_timers_[type]->set_interval(UpkeepInterval);
|
||||
|
||||
tr_logAddInfo(fmt::format(
|
||||
_("Successfully updated global {type} address to {ip} using {url}"),
|
||||
fmt::arg("type", protocol),
|
||||
fmt::arg("ip", addr->display_name()),
|
||||
fmt::arg("url", IPQueryServices[type][ix_service_[type]])));
|
||||
}
|
||||
}
|
||||
|
||||
// Try next IP query URL
|
||||
if (!success && ++ix_service_[type] < std::size(IPQueryServices[type]))
|
||||
{
|
||||
update_global_addr(type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!success)
|
||||
{
|
||||
tr_logAddDebug(fmt::format("Couldn't obtain global {} address", protocol));
|
||||
unset_global_addr(type);
|
||||
upkeep_timers_[type]->set_interval(RetryUpkeepInterval);
|
||||
}
|
||||
|
||||
ix_service_[type] = 0U;
|
||||
unset_is_updating(type);
|
||||
}
|
||||
|
||||
void tr_global_ip_cache::unset_global_addr(tr_address_type type) noexcept
|
||||
{
|
||||
auto const lock = std::lock_guard{ global_addr_mutex_[type] };
|
||||
global_addr_[type].reset();
|
||||
tr_logAddTrace(fmt::format("Unset {} global address cache", type == TR_AF_INET ? "IPv4"sv : "IPv6"sv));
|
||||
tr_logAddTrace(fmt::format("Unset {} global address cache", protocol_str(type)));
|
||||
}
|
||||
|
||||
void tr_global_ip_cache::set_source_addr(tr_address const& addr) noexcept
|
||||
@@ -225,7 +346,7 @@ void tr_global_ip_cache::unset_addr(tr_address_type type) noexcept
|
||||
{
|
||||
auto const lock = std::lock_guard{ source_addr_mutex_[type] };
|
||||
source_addr_[type].reset();
|
||||
tr_logAddTrace(fmt::format("Unset {} source address cache", type == TR_AF_INET ? "IPv4"sv : "IPv6"sv));
|
||||
tr_logAddTrace(fmt::format("Unset {} source address cache", protocol_str(type)));
|
||||
|
||||
// No public internet connectivity means no global IP address
|
||||
unset_global_addr(type);
|
||||
@@ -255,109 +376,3 @@ void tr_global_ip_cache::unset_is_updating(tr_address_type type) noexcept
|
||||
lock.unlock();
|
||||
is_updating_cv_[type].notify_one();
|
||||
}
|
||||
|
||||
void tr_global_ip_cache::update_global_addr(tr_address_type type) noexcept
|
||||
{
|
||||
TR_ASSERT(has_ip_protocol_[type]);
|
||||
TR_ASSERT(global_source_addr(type));
|
||||
TR_ASSERT(ix_service_[type] < std::size(IPQueryServices));
|
||||
|
||||
if (ix_service_[type] == 0U && !set_is_updating(type))
|
||||
{
|
||||
return;
|
||||
}
|
||||
TR_ASSERT(is_updating_[type] == is_updating_t::YES);
|
||||
|
||||
// Update global address
|
||||
auto options = tr_web::FetchOptions{ IPQueryServices[ix_service_[type]],
|
||||
[this, type](tr_web::FetchResponse const& response)
|
||||
{ this->on_response_ip_query(type, response); },
|
||||
nullptr };
|
||||
options.ip_proto = type == TR_AF_INET ? tr_web::FetchOptions::IPProtocol::V4 : tr_web::FetchOptions::IPProtocol::V6;
|
||||
options.sndbuf = 4096;
|
||||
options.rcvbuf = 4096;
|
||||
web_.fetch(std::move(options));
|
||||
}
|
||||
|
||||
void tr_global_ip_cache::update_source_addr(tr_address_type type) noexcept
|
||||
{
|
||||
using namespace external_source_ip_helpers;
|
||||
|
||||
TR_ASSERT(has_ip_protocol_[type]);
|
||||
|
||||
if (!set_is_updating(type))
|
||||
{
|
||||
return;
|
||||
}
|
||||
TR_ASSERT(is_updating_[type] == is_updating_t::YES);
|
||||
|
||||
auto const protocol = type == TR_AF_INET ? "IPv4"sv : "IPv6"sv;
|
||||
|
||||
auto err = int{};
|
||||
auto const& source_addr = get_global_source_address(bind_addr(type), err);
|
||||
if (source_addr)
|
||||
{
|
||||
set_source_addr(*source_addr);
|
||||
tr_logAddInfo(fmt::format(
|
||||
_("Successfully updated source {protocol} address to {ip}"),
|
||||
fmt::arg("protocol", protocol),
|
||||
fmt::arg("ip", source_addr->display_name())));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stop the update process since we have no public internet connectivity
|
||||
unset_addr(type);
|
||||
upkeep_timers_[type]->set_interval(RetryUpkeepInterval);
|
||||
tr_logAddDebug(fmt::format(_("Couldn't obtain source {protocol} address"), fmt::arg("protocol", protocol)));
|
||||
if (err == EAFNOSUPPORT)
|
||||
{
|
||||
stop_timer(type); // No point in retrying
|
||||
has_ip_protocol_[type] = false;
|
||||
tr_logAddInfo(fmt::format(_("Your machine does not support {protocol}"), fmt::arg("protocol", protocol)));
|
||||
}
|
||||
}
|
||||
|
||||
unset_is_updating(type);
|
||||
}
|
||||
|
||||
void tr_global_ip_cache::on_response_ip_query(tr_address_type type, tr_web::FetchResponse const& response) noexcept
|
||||
{
|
||||
TR_ASSERT(is_updating_[type] == is_updating_t::YES);
|
||||
TR_ASSERT(ix_service_[type] < std::size(IPQueryServices));
|
||||
|
||||
auto const protocol = type == TR_AF_INET ? "IPv4"sv : "IPv6"sv;
|
||||
auto success = bool{ false };
|
||||
|
||||
if (response.status == 200 /* HTTP_OK */)
|
||||
{
|
||||
// Update member
|
||||
if (auto addr = tr_address::from_string(tr_strvStrip(response.body)); addr && set_global_addr(type, *addr))
|
||||
{
|
||||
success = true;
|
||||
upkeep_timers_[type]->set_interval(UpkeepInterval);
|
||||
|
||||
tr_logAddInfo(fmt::format(
|
||||
_("Successfully updated global {type} address to {ip} using {url}"),
|
||||
fmt::arg("type", protocol),
|
||||
fmt::arg("ip", addr->display_name()),
|
||||
fmt::arg("url", IPQueryServices[ix_service_[type]])));
|
||||
}
|
||||
}
|
||||
|
||||
// Try next IP query URL
|
||||
if (!success && ++ix_service_[type] < std::size(IPQueryServices))
|
||||
{
|
||||
update_global_addr(type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!success)
|
||||
{
|
||||
tr_logAddDebug(fmt::format("Couldn't obtain global {} address", protocol));
|
||||
unset_global_addr(type);
|
||||
upkeep_timers_[type]->set_interval(RetryUpkeepInterval);
|
||||
}
|
||||
|
||||
ix_service_[type] = 0U;
|
||||
unset_is_updating(type);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "libtransmission/net.h"
|
||||
@@ -34,13 +33,34 @@
|
||||
*
|
||||
* The idea is, if this class successfully cached a source address, that means
|
||||
* you have connectivity to the public internet. And if the global address is
|
||||
* the same with the source address, then you are not behind an NAT.
|
||||
* the same as the source address, then you are not behind a NAT.
|
||||
*
|
||||
*/
|
||||
class tr_global_ip_cache
|
||||
{
|
||||
public:
|
||||
tr_global_ip_cache(tr_web& web_in, libtransmission::TimerMaker& timer_maker_in);
|
||||
struct Mediator
|
||||
{
|
||||
virtual ~Mediator() = default;
|
||||
|
||||
virtual void fetch(tr_web::FetchOptions&& /* options */)
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] virtual std::string_view settings_bind_addr(tr_address_type /* type */)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] virtual libtransmission::TimerMaker& timer_maker() = 0;
|
||||
};
|
||||
|
||||
private:
|
||||
explicit tr_global_ip_cache(Mediator& mediator_in);
|
||||
|
||||
public:
|
||||
[[nodiscard]] static std::unique_ptr<tr_global_ip_cache> create(Mediator& mediator_in);
|
||||
|
||||
tr_global_ip_cache() = delete;
|
||||
~tr_global_ip_cache();
|
||||
tr_global_ip_cache(tr_global_ip_cache const&) = delete;
|
||||
@@ -62,12 +82,17 @@ public:
|
||||
return source_addr_[type];
|
||||
}
|
||||
|
||||
void set_settings_bind_addr(tr_address_type type, std::string_view bind_address) noexcept;
|
||||
[[nodiscard]] tr_address bind_addr(tr_address_type type) const noexcept;
|
||||
|
||||
void update_addr(tr_address_type type) noexcept;
|
||||
bool set_global_addr(tr_address_type type, tr_address const& addr) noexcept;
|
||||
|
||||
void update_addr(tr_address_type type) noexcept;
|
||||
void update_global_addr(tr_address_type type) noexcept;
|
||||
void update_source_addr(tr_address_type type) noexcept;
|
||||
|
||||
// Only use as a callback for web_->fetch()
|
||||
void on_response_ip_query(tr_address_type type, tr_web::FetchResponse const& response) noexcept;
|
||||
|
||||
[[nodiscard]] constexpr auto has_ip_protocol(tr_address_type type) const noexcept
|
||||
{
|
||||
return has_ip_protocol_[type];
|
||||
@@ -94,19 +119,11 @@ private:
|
||||
[[nodiscard]] bool set_is_updating(tr_address_type type) noexcept;
|
||||
void unset_is_updating(tr_address_type type) noexcept;
|
||||
|
||||
void update_global_addr(tr_address_type type) noexcept;
|
||||
void update_source_addr(tr_address_type type) noexcept;
|
||||
|
||||
// Only use as a callback for web_->fetch()
|
||||
void on_response_ip_query(tr_address_type type, tr_web::FetchResponse const& response) noexcept;
|
||||
|
||||
tr_web& web_;
|
||||
|
||||
array_ip_t<std::optional<tr_address>> settings_bind_addr_;
|
||||
Mediator& mediator_;
|
||||
|
||||
enum class is_updating_t
|
||||
{
|
||||
NO,
|
||||
NO = 0,
|
||||
YES,
|
||||
ABORT
|
||||
};
|
||||
|
||||
@@ -961,10 +961,14 @@ void sendLtepHandshake(tr_peerMsgsImpl* msgs)
|
||||
tr_variantInitDict(&val, 8);
|
||||
tr_variantDictAddBool(&val, TR_KEY_e, msgs->session->encryptionMode() != TR_CLEAR_PREFERRED);
|
||||
|
||||
if (auto const addr = msgs->session->publicAddress(TR_AF_INET6); !addr.is_any())
|
||||
// If connecting to global peer, then use global address
|
||||
// Otherwise we are connecting to local peer, use bind address directly
|
||||
if (auto const addr = msgs->io->address().is_global_unicast_address() ? msgs->session->global_address(TR_AF_INET6) :
|
||||
msgs->session->publicAddress(TR_AF_INET6);
|
||||
addr && !addr->is_any())
|
||||
{
|
||||
TR_ASSERT(addr.is_ipv6());
|
||||
tr_variantDictAddRaw(&val, TR_KEY_ipv6, &addr.addr.addr6, sizeof(addr.addr.addr6));
|
||||
TR_ASSERT(addr->is_ipv6());
|
||||
tr_variantDictAddRaw(&val, TR_KEY_ipv6, &addr->addr.addr6, sizeof(addr->addr.addr6));
|
||||
}
|
||||
|
||||
// http://bittorrent.org/beps/bep_0009.html
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
// FIXME(ckerr) do not merge these three lines.
|
||||
// This comment is to make CI think libtransmission has
|
||||
// changed so that it will run the libtransmission CI tests
|
||||
|
||||
namespace
|
||||
{
|
||||
#ifdef _WIN32
|
||||
|
||||
@@ -18,7 +18,7 @@ using namespace std::literals;
|
||||
namespace
|
||||
{
|
||||
|
||||
auto constexpr MyStatic = std::array<std::string_view, 402>{ ""sv,
|
||||
auto constexpr MyStatic = std::array<std::string_view, 404>{ ""sv,
|
||||
"activeTorrentCount"sv,
|
||||
"activity-date"sv,
|
||||
"activityDate"sv,
|
||||
@@ -47,6 +47,7 @@ auto constexpr MyStatic = std::array<std::string_view, 402>{ ""sv,
|
||||
"availability"sv,
|
||||
"bandwidth-priority"sv,
|
||||
"bandwidthPriority"sv,
|
||||
"beginPiece"sv,
|
||||
"bind-address-ipv4"sv,
|
||||
"bind-address-ipv6"sv,
|
||||
"bitfield"sv,
|
||||
@@ -108,6 +109,7 @@ auto constexpr MyStatic = std::array<std::string_view, 402>{ ""sv,
|
||||
"editDate"sv,
|
||||
"encoding"sv,
|
||||
"encryption"sv,
|
||||
"endPiece"sv,
|
||||
"error"sv,
|
||||
"errorString"sv,
|
||||
"eta"sv,
|
||||
|
||||
@@ -50,6 +50,7 @@ enum
|
||||
TR_KEY_availability, // rpc
|
||||
TR_KEY_bandwidth_priority,
|
||||
TR_KEY_bandwidthPriority,
|
||||
TR_KEY_beginPiece,
|
||||
TR_KEY_bind_address_ipv4,
|
||||
TR_KEY_bind_address_ipv6,
|
||||
TR_KEY_bitfield,
|
||||
@@ -111,6 +112,7 @@ enum
|
||||
TR_KEY_editDate,
|
||||
TR_KEY_encoding,
|
||||
TR_KEY_encryption,
|
||||
TR_KEY_endPiece,
|
||||
TR_KEY_error,
|
||||
TR_KEY_errorString,
|
||||
TR_KEY_eta,
|
||||
|
||||
@@ -77,40 +77,102 @@ auto inline constexpr TrUnixAddrStrLen = size_t{ sizeof(((struct sockaddr_un*)nu
|
||||
|
||||
enum tr_rpc_address_type
|
||||
{
|
||||
TR_RPC_AF_INET,
|
||||
TR_RPC_AF_INET6,
|
||||
TR_RPC_AF_UNIX
|
||||
TR_RPC_INET_ADDR,
|
||||
TR_RPC_UNIX_ADDR
|
||||
};
|
||||
|
||||
class tr_unix_addr
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] std::string to_string() const
|
||||
{
|
||||
return std::empty(unix_socket_path_) ? unix_socket_path_ : std::string(TrUnixSocketPrefix);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool from_string(std::string_view src)
|
||||
{
|
||||
if (!tr_strvStartsWith(TrUnixSocketPrefix, src))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (std::size(src) >= TrUnixAddrStrLen)
|
||||
{
|
||||
tr_logAddError(fmt::format(
|
||||
_("Unix socket path must be fewer than {count} characters (including '{prefix}' prefix)"),
|
||||
fmt::arg("count", TrUnixAddrStrLen - 1),
|
||||
fmt::arg("prefix", TrUnixSocketPrefix)));
|
||||
return false;
|
||||
}
|
||||
unix_socket_path_ = src;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string unix_socket_path_ = {};
|
||||
};
|
||||
} // namespace
|
||||
|
||||
struct tr_rpc_address
|
||||
class tr_rpc_address
|
||||
{
|
||||
tr_rpc_address_type type;
|
||||
union
|
||||
public:
|
||||
tr_rpc_address()
|
||||
: inet_addr_(tr_address::any_ipv4())
|
||||
{
|
||||
struct in_addr addr4;
|
||||
struct in6_addr addr6;
|
||||
std::array<char, TrUnixAddrStrLen> unixSocketPath;
|
||||
} addr;
|
||||
|
||||
void set_inaddr_any()
|
||||
{
|
||||
type = TR_RPC_AF_INET;
|
||||
addr.addr4 = { INADDR_ANY };
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto is_unix_addr() const noexcept
|
||||
{
|
||||
return type_ == TR_RPC_UNIX_ADDR;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto is_inet_addr() const noexcept
|
||||
{
|
||||
return type_ == TR_RPC_INET_ADDR;
|
||||
}
|
||||
|
||||
bool from_string(std::string_view src)
|
||||
{
|
||||
if (auto address = tr_address::from_string(src); address.has_value())
|
||||
{
|
||||
type_ = TR_RPC_INET_ADDR;
|
||||
inet_addr_ = address.value();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (unix_addr_.from_string(src))
|
||||
{
|
||||
type_ = TR_RPC_UNIX_ADDR;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string to_string(tr_port port = {}) const
|
||||
{
|
||||
if (type_ == TR_RPC_UNIX_ADDR)
|
||||
{
|
||||
return unix_addr_.to_string();
|
||||
}
|
||||
|
||||
if (port.empty())
|
||||
{
|
||||
return { inet_addr_.display_name() };
|
||||
}
|
||||
return { inet_addr_.display_name(port) };
|
||||
}
|
||||
|
||||
private:
|
||||
tr_rpc_address_type type_ = TR_RPC_INET_ADDR;
|
||||
struct tr_address inet_addr_;
|
||||
class tr_unix_addr unix_addr_;
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
int constexpr DeflateLevel = 6; // medium / default
|
||||
|
||||
#ifdef TR_ENABLE_ASSERTS
|
||||
bool constexpr tr_rpc_address_is_valid(tr_rpc_address const& a)
|
||||
{
|
||||
return a.type == TR_RPC_AF_INET || a.type == TR_RPC_AF_INET6 || a.type == TR_RPC_AF_UNIX;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---
|
||||
|
||||
void send_simple_response(struct evhttp_request* req, int code, char const* text = nullptr)
|
||||
@@ -534,73 +596,6 @@ auto constexpr ServerStartRetryCount = int{ 10 };
|
||||
auto constexpr ServerStartRetryDelayIncrement = 5s;
|
||||
auto constexpr ServerStartRetryMaxDelay = 60s;
|
||||
|
||||
char const* tr_rpc_address_to_string(tr_rpc_address const& addr, char* buf, size_t buflen)
|
||||
{
|
||||
TR_ASSERT(tr_rpc_address_is_valid(addr));
|
||||
|
||||
switch (addr.type)
|
||||
{
|
||||
case TR_RPC_AF_INET:
|
||||
return evutil_inet_ntop(AF_INET, &addr.addr, buf, buflen);
|
||||
|
||||
case TR_RPC_AF_INET6:
|
||||
return evutil_inet_ntop(AF_INET6, &addr.addr, buf, buflen);
|
||||
|
||||
case TR_RPC_AF_UNIX:
|
||||
tr_strlcpy(buf, std::data(addr.addr.unixSocketPath), buflen);
|
||||
return buf;
|
||||
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::string tr_rpc_address_with_port(tr_rpc_server const* server)
|
||||
{
|
||||
auto addr_buf = std::array<char, TrUnixAddrStrLen>{};
|
||||
tr_rpc_address_to_string(*server->bind_address_, std::data(addr_buf), std::size(addr_buf));
|
||||
|
||||
std::string addr_port_str = std::data(addr_buf);
|
||||
if (server->bind_address_->type != TR_RPC_AF_UNIX)
|
||||
{
|
||||
addr_port_str.append(":" + std::to_string(server->port().host()));
|
||||
}
|
||||
return addr_port_str;
|
||||
}
|
||||
|
||||
bool tr_rpc_address_from_string(tr_rpc_address& dst, std::string_view src)
|
||||
{
|
||||
if (tr_strvStartsWith(src, TrUnixSocketPrefix))
|
||||
{
|
||||
if (std::size(src) >= TrUnixAddrStrLen)
|
||||
{
|
||||
tr_logAddError(fmt::format(
|
||||
_("Unix socket path must be fewer than {count} characters (including '{prefix}' prefix)"),
|
||||
fmt::arg("count", TrUnixAddrStrLen - 1),
|
||||
fmt::arg("prefix", TrUnixSocketPrefix)));
|
||||
return false;
|
||||
}
|
||||
|
||||
dst.type = TR_RPC_AF_UNIX;
|
||||
tr_strlcpy(std::data(dst.addr.unixSocketPath), std::string{ src }.c_str(), std::size(dst.addr.unixSocketPath));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (evutil_inet_pton(AF_INET, std::string{ src }.c_str(), &dst.addr) == 1)
|
||||
{
|
||||
dst.type = TR_RPC_AF_INET;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (evutil_inet_pton(AF_INET6, std::string{ src }.c_str(), &dst.addr) == 1)
|
||||
{
|
||||
dst.type = TR_RPC_AF_INET6;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool bindUnixSocket(
|
||||
[[maybe_unused]] struct event_base* base,
|
||||
[[maybe_unused]] struct evhttp* httpd,
|
||||
@@ -679,11 +674,11 @@ void start_server(tr_rpc_server* server)
|
||||
auto const address = server->get_bind_address();
|
||||
auto const port = server->port();
|
||||
|
||||
bool const success = server->bind_address_->type == TR_RPC_AF_UNIX ?
|
||||
bool const success = server->bind_address_->is_unix_addr() ?
|
||||
bindUnixSocket(base, httpd, address.c_str(), server->socket_mode_) :
|
||||
(evhttp_bind_socket(httpd, address.c_str(), port.host()) != -1);
|
||||
|
||||
auto const addr_port_str = tr_rpc_address_with_port(server);
|
||||
auto const addr_port_str = server->bind_address_->to_string(port);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
@@ -732,14 +727,14 @@ void stop_server(tr_rpc_server* server)
|
||||
|
||||
httpd.reset();
|
||||
|
||||
if (server->bind_address_->type == TR_RPC_AF_UNIX)
|
||||
if (server->bind_address_->is_unix_addr())
|
||||
{
|
||||
unlink(address.c_str() + std::size(TrUnixSocketPrefix));
|
||||
}
|
||||
|
||||
tr_logAddInfo(fmt::format(
|
||||
_("Stopped listening for RPC and Web requests on '{address}'"),
|
||||
fmt::arg("address", tr_rpc_address_with_port(server))));
|
||||
fmt::arg("address", server->bind_address_->to_string(server->port()))));
|
||||
}
|
||||
|
||||
void restart_server(tr_rpc_server* const server)
|
||||
@@ -838,8 +833,7 @@ void tr_rpc_server::set_password_enabled(bool enabled)
|
||||
|
||||
std::string tr_rpc_server::get_bind_address() const
|
||||
{
|
||||
auto buf = std::array<char, TrUnixAddrStrLen>{};
|
||||
return tr_rpc_address_to_string(*this->bind_address_, std::data(buf), std::size(buf));
|
||||
return bind_address_->to_string();
|
||||
}
|
||||
|
||||
void tr_rpc_server::set_anti_brute_force_enabled(bool enabled) noexcept
|
||||
@@ -857,7 +851,7 @@ void tr_rpc_server::set_anti_brute_force_enabled(bool enabled) noexcept
|
||||
tr_rpc_server::tr_rpc_server(tr_session* session_in, tr_variant* settings)
|
||||
: compressor{ libdeflate_alloc_compressor(DeflateLevel), libdeflate_free_compressor }
|
||||
, web_client_dir_{ tr_getWebClientDir(session_in) }
|
||||
, bind_address_(std::make_unique<struct tr_rpc_address>())
|
||||
, bind_address_(std::make_unique<class tr_rpc_address>())
|
||||
, session{ session_in }
|
||||
{
|
||||
load(settings);
|
||||
@@ -887,23 +881,23 @@ void tr_rpc_server::load(tr_variant* src)
|
||||
this->set_username(username_);
|
||||
this->set_password(salted_password_);
|
||||
|
||||
if (!tr_rpc_address_from_string(*bind_address_, bind_address_str_))
|
||||
if (!bind_address_->from_string(bind_address_str_))
|
||||
{
|
||||
// NOTE: bind_address_ is default initialized to INADDR_ANY
|
||||
tr_logAddWarn(fmt::format(
|
||||
_("The '{key}' setting is '{value}' but must be an IPv4 or IPv6 address or a Unix socket path. Using default value '0.0.0.0'"),
|
||||
fmt::format("key", tr_quark_get_string_view(TR_KEY_rpc_bind_address)),
|
||||
fmt::format("value", bind_address_str_)));
|
||||
bind_address_->set_inaddr_any();
|
||||
}
|
||||
|
||||
if (bind_address_->type == TR_RPC_AF_UNIX)
|
||||
if (bind_address_->is_unix_addr())
|
||||
{
|
||||
this->set_whitelist_enabled(false);
|
||||
this->is_host_whitelist_enabled_ = false;
|
||||
}
|
||||
if (this->is_enabled())
|
||||
{
|
||||
auto const rpc_uri = tr_rpc_address_with_port(this) + this->url_;
|
||||
auto const rpc_uri = bind_address_->to_string(this->port()) + this->url_;
|
||||
tr_logAddInfo(fmt::format(_("Serving RPC and Web requests on {address}"), fmt::arg("address", rpc_uri)));
|
||||
session->runInSessionThread(start_server, this);
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
struct evhttp;
|
||||
struct tr_variant;
|
||||
struct tr_rpc_address;
|
||||
class tr_rpc_address;
|
||||
struct libdeflate_compressor;
|
||||
|
||||
namespace libtransmission
|
||||
@@ -153,7 +153,7 @@ public:
|
||||
std::vector<std::string> whitelist_;
|
||||
std::string const web_client_dir_;
|
||||
|
||||
std::unique_ptr<struct tr_rpc_address> bind_address_;
|
||||
std::unique_ptr<class tr_rpc_address> bind_address_;
|
||||
|
||||
std::unique_ptr<libtransmission::Timer> start_retry_timer;
|
||||
libtransmission::evhelpers::evhttp_unique_ptr httpd;
|
||||
|
||||
@@ -326,8 +326,10 @@ void addFiles(tr_torrent const* tor, tr_variant* list)
|
||||
for (tr_file_index_t i = 0, n = tor->file_count(); i < n; ++i)
|
||||
{
|
||||
auto const file = tr_torrentFile(tor, i);
|
||||
tr_variant* d = tr_variantListAddDict(list, 3);
|
||||
tr_variant* d = tr_variantListAddDict(list, 5);
|
||||
tr_variantDictAddInt(d, TR_KEY_beginPiece, file.beginPiece);
|
||||
tr_variantDictAddInt(d, TR_KEY_bytesCompleted, file.have);
|
||||
tr_variantDictAddInt(d, TR_KEY_endPiece, file.endPiece);
|
||||
tr_variantDictAddInt(d, TR_KEY_length, file.length);
|
||||
tr_variantDictAddStr(d, TR_KEY_name, file.name);
|
||||
}
|
||||
|
||||
@@ -432,7 +432,9 @@ tr_address tr_session::publicAddress(tr_address_type type) const noexcept
|
||||
// otherwise, if we can determine which one to use via global_source_address(ipv6) magic, use it.
|
||||
// otherwise, use any_ipv6 (::).
|
||||
auto const source_addr = global_source_address(type);
|
||||
return source_addr && source_addr->is_global_unicast_address() ? *source_addr : global_ip_cache_->bind_addr(type);
|
||||
auto const default_addr = source_addr && source_addr->is_global_unicast_address() ? *source_addr :
|
||||
tr_address::any_ipv6();
|
||||
return tr_address::from_string(settings_.bind_address_ipv6).value_or(default_addr);
|
||||
}
|
||||
|
||||
TR_ASSERT_MSG(false, "invalid type");
|
||||
@@ -639,8 +641,6 @@ void tr_session::initImpl(init_data& data)
|
||||
|
||||
this->blocklists_ = libtransmission::Blocklist::loadBlocklists(blocklist_dir_, useBlocklist());
|
||||
|
||||
this->global_ip_cache_ = std::make_unique<tr_global_ip_cache>(*web_, timerMaker());
|
||||
|
||||
tr_logAddInfo(fmt::format(_("Transmission version {version} starting"), fmt::arg("version", LONG_VERSION_STRING)));
|
||||
|
||||
setSettings(client_settings, true);
|
||||
@@ -701,11 +701,11 @@ void tr_session::setSettings(tr_session_settings&& settings_in, bool force)
|
||||
|
||||
if (auto const& val = new_settings.bind_address_ipv4; force || val != old_settings.bind_address_ipv4)
|
||||
{
|
||||
global_ip_cache_->set_settings_bind_addr(TR_AF_INET, new_settings.bind_address_ipv4);
|
||||
global_ip_cache_->update_addr(TR_AF_INET);
|
||||
}
|
||||
if (auto const& val = new_settings.bind_address_ipv6; force || val != old_settings.bind_address_ipv6)
|
||||
{
|
||||
global_ip_cache_->set_settings_bind_addr(TR_AF_INET6, new_settings.bind_address_ipv6);
|
||||
global_ip_cache_->update_addr(TR_AF_INET6);
|
||||
}
|
||||
|
||||
if (auto const& val = new_settings.default_trackers_str; force || val != old_settings.default_trackers_str)
|
||||
|
||||
@@ -275,6 +275,42 @@ private:
|
||||
tr_session& session_;
|
||||
};
|
||||
|
||||
class GlobalIPCacheMediator final : public tr_global_ip_cache::Mediator
|
||||
{
|
||||
public:
|
||||
explicit GlobalIPCacheMediator(tr_session& session) noexcept
|
||||
: session_(session)
|
||||
{
|
||||
}
|
||||
|
||||
void fetch(tr_web::FetchOptions&& options) override
|
||||
{
|
||||
session_.fetch(std::move(options));
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string_view settings_bind_addr(tr_address_type type) override
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TR_AF_INET:
|
||||
return session_.settings_.bind_address_ipv4;
|
||||
case TR_AF_INET6:
|
||||
return session_.settings_.bind_address_ipv6;
|
||||
default:
|
||||
TR_ASSERT_MSG(false, "Invalid type");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] libtransmission::TimerMaker& timer_maker() override
|
||||
{
|
||||
return session_.timerMaker();
|
||||
}
|
||||
|
||||
private:
|
||||
tr_session& session_;
|
||||
};
|
||||
|
||||
// UDP connectivity used for the DHT and µTP
|
||||
class tr_udp_core
|
||||
{
|
||||
@@ -1112,7 +1148,8 @@ private:
|
||||
tr_torrents torrents_;
|
||||
|
||||
// depends-on: settings_, session_thread_, timer_maker_, web_
|
||||
std::unique_ptr<tr_global_ip_cache> global_ip_cache_;
|
||||
GlobalIPCacheMediator global_ip_cache_mediator_{ *this };
|
||||
std::unique_ptr<tr_global_ip_cache> global_ip_cache_ = tr_global_ip_cache::create(global_ip_cache_mediator_);
|
||||
|
||||
// depends-on: settings_, session_thread_, torrents_, global_ip_cache (via tr_session::publicAddress())
|
||||
WebMediator web_mediator_{ this };
|
||||
|
||||
@@ -1615,14 +1615,15 @@ tr_file_view tr_torrentFile(tr_torrent const* tor, tr_file_index_t file)
|
||||
auto const priority = tor->file_priorities_.file_priority(file);
|
||||
auto const wanted = tor->files_wanted_.file_wanted(file);
|
||||
auto const length = tor->file_size(file);
|
||||
auto const [begin, end] = tor->pieces_in_file(file);
|
||||
|
||||
if (tor->completeness == TR_SEED || length == 0)
|
||||
{
|
||||
return { subpath.c_str(), length, length, 1.0, priority, wanted };
|
||||
return { subpath.c_str(), length, length, 1.0, begin, end, priority, wanted };
|
||||
}
|
||||
|
||||
auto const have = tor->completion.count_has_bytes_in_span(tor->fpm_.byte_span(file));
|
||||
return { subpath.c_str(), have, length, have >= length ? 1.0 : have / double(length), priority, wanted };
|
||||
return { subpath.c_str(), have, length, have >= length ? 1.0 : have / double(length), begin, end, priority, wanted };
|
||||
}
|
||||
|
||||
size_t tr_torrentFileCount(tr_torrent const* torrent)
|
||||
@@ -2446,10 +2447,36 @@ namespace
|
||||
{
|
||||
namespace rename_helpers
|
||||
{
|
||||
bool renameArgsAreValid(std::string_view oldpath, std::string_view newname)
|
||||
bool renameArgsAreValid(tr_torrent const* tor, std::string_view oldpath, std::string_view newname)
|
||||
{
|
||||
return !std::empty(oldpath) && !std::empty(newname) && newname != "."sv && newname != ".."sv &&
|
||||
!tr_strvContains(newname, TR_PATH_DELIMITER);
|
||||
if (std::empty(oldpath) || std::empty(newname) || newname == "."sv || newname == ".."sv ||
|
||||
tr_strvContains(newname, TR_PATH_DELIMITER))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const newpath = tr_strvContains(oldpath, TR_PATH_DELIMITER) ?
|
||||
tr_pathbuf{ tr_sys_path_dirname(oldpath), '/', newname } :
|
||||
tr_pathbuf{ newname };
|
||||
|
||||
if (newpath == oldpath)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
auto const newpath_as_dir = tr_pathbuf{ newpath, '/' };
|
||||
auto const n_files = tor->file_count();
|
||||
|
||||
for (tr_file_index_t i = 0; i < n_files; ++i)
|
||||
{
|
||||
auto const& name = tor->file_subpath(i);
|
||||
if (newpath == name || tr_strvStartsWith(name, newpath_as_dir))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto renameFindAffectedFiles(tr_torrent const* tor, std::string_view oldpath)
|
||||
@@ -2566,7 +2593,7 @@ void torrentRenamePath(
|
||||
|
||||
int error = 0;
|
||||
|
||||
if (!renameArgsAreValid(oldpath, newname))
|
||||
if (!renameArgsAreValid(tor, oldpath, newname))
|
||||
{
|
||||
error = EINVAL;
|
||||
}
|
||||
|
||||
@@ -1334,6 +1334,8 @@ struct tr_file_view
|
||||
uint64_t have; // the current size of the file, i.e. how much we've downloaded
|
||||
uint64_t length; // the total size of the file
|
||||
double progress; // have / length
|
||||
tr_piece_index_t beginPiece; // piece index where this file starts
|
||||
tr_piece_index_t endPiece; // piece index where this file ends (exclusive)
|
||||
tr_priority_t priority; // the file's priority
|
||||
bool wanted; // do we want to download this file?
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <cerrno>
|
||||
#include <cfloat> // DBL_DIG
|
||||
#include <chrono>
|
||||
#include <clocale> // localeconv()
|
||||
#include <cstdint> // SIZE_MAX
|
||||
#include <cstdlib> // getenv()
|
||||
#include <cstring> /* strerror() */
|
||||
@@ -514,10 +513,10 @@ std::vector<int> tr_parseNumberRange(std::string_view str)
|
||||
double tr_truncd(double x, int decimal_places)
|
||||
{
|
||||
auto buf = std::array<char, 128>{};
|
||||
auto const [out, len] = fmt::format_to_n(std::data(buf), std::size(buf) - 1, "{:.{}Lf}", x, DBL_DIG);
|
||||
auto const [out, len] = fmt::format_to_n(std::data(buf), std::size(buf) - 1, "{:.{}f}", x, DBL_DIG);
|
||||
*out = '\0';
|
||||
|
||||
if (auto* const pt = strstr(std::data(buf), localeconv()->decimal_point); pt != nullptr)
|
||||
if (auto* const pt = strchr(std::data(buf), '.'); pt != nullptr)
|
||||
{
|
||||
pt[decimal_places != 0 ? decimal_places + 1 : 0] = '\0';
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ void error_handler(jsonsl_t jsn, jsonsl_error_t error, jsonsl_state_st* /*state*
|
||||
fmt::arg("position", jsn->pos),
|
||||
fmt::arg("text", std::string_view{ buf, std::min(size_t{ 16U }, data->size - jsn->pos) }),
|
||||
fmt::arg("error", jsonsl_strerror(error)),
|
||||
fmt::arg("error_code", error)));
|
||||
fmt::arg("error_code", static_cast<int>(error))));
|
||||
}
|
||||
|
||||
int error_callback(jsonsl_t jsn, jsonsl_error_t error, struct jsonsl_state_st* state, jsonsl_char_t* at)
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
typedef NS_ENUM(NSInteger, AttributesStyle) {
|
||||
AttributesStyleNormal,
|
||||
AttributesStyleEmphasized,
|
||||
AttributesStyleDisabled,
|
||||
};
|
||||
|
||||
@interface FileNameCell : NSActionCell
|
||||
|
||||
- (NSRect)imageRectForBounds:(NSRect)bounds;
|
||||
|
||||
@@ -20,46 +20,52 @@ static CGFloat const kPaddingBelowStatusFile = 2.0;
|
||||
static CGFloat const kPaddingBetweenNameAndFolderStatus = 4.0;
|
||||
static CGFloat const kPaddingExpansionFrame = 2.0;
|
||||
|
||||
@interface FileNameCell ()
|
||||
static NSMutableParagraphStyle* sParagraphStyle()
|
||||
{
|
||||
NSMutableParagraphStyle* paragraphStyle = [NSParagraphStyle.defaultParagraphStyle mutableCopy];
|
||||
paragraphStyle.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
return paragraphStyle;
|
||||
}
|
||||
static NSMutableParagraphStyle* sStatusParagraphStyle()
|
||||
{
|
||||
NSMutableParagraphStyle* paragraphStyle = [NSParagraphStyle.defaultParagraphStyle mutableCopy];
|
||||
paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
return paragraphStyle;
|
||||
}
|
||||
|
||||
@property(nonatomic, readonly) NSAttributedString* attributedTitle;
|
||||
@property(nonatomic, readonly) NSAttributedString* attributedStatus;
|
||||
@property(nonatomic, readonly) NSMutableDictionary* fTitleAttributes;
|
||||
@property(nonatomic, readonly) NSMutableDictionary* fStatusAttributes;
|
||||
|
||||
@end
|
||||
static NSDictionary<NSAttributedStringKey, id>* const kTitleAttributes = @{
|
||||
NSFontAttributeName : [NSFont messageFontOfSize:12.0],
|
||||
NSParagraphStyleAttributeName : sParagraphStyle(),
|
||||
NSForegroundColorAttributeName : NSColor.controlTextColor
|
||||
};
|
||||
static NSDictionary<NSAttributedStringKey, id>* const kStatusAttributes = @{
|
||||
NSFontAttributeName : [NSFont messageFontOfSize:9.0],
|
||||
NSParagraphStyleAttributeName : sStatusParagraphStyle(),
|
||||
NSForegroundColorAttributeName : NSColor.secondaryLabelColor
|
||||
};
|
||||
static NSDictionary<NSAttributedStringKey, id>* const kTitleEmphasizedAttributes = @{
|
||||
NSFontAttributeName : [NSFont messageFontOfSize:12.0],
|
||||
NSParagraphStyleAttributeName : sParagraphStyle(),
|
||||
NSForegroundColorAttributeName : NSColor.whiteColor
|
||||
};
|
||||
static NSDictionary<NSAttributedStringKey, id>* const kStatusEmphasizedAttributes = @{
|
||||
NSFontAttributeName : [NSFont messageFontOfSize:9.0],
|
||||
NSParagraphStyleAttributeName : sStatusParagraphStyle(),
|
||||
NSForegroundColorAttributeName : NSColor.whiteColor
|
||||
};
|
||||
static NSDictionary<NSAttributedStringKey, id>* const kTitleDisabledAttributes = @{
|
||||
NSFontAttributeName : [NSFont messageFontOfSize:12.0],
|
||||
NSParagraphStyleAttributeName : sParagraphStyle(),
|
||||
NSForegroundColorAttributeName : NSColor.disabledControlTextColor
|
||||
};
|
||||
static NSDictionary<NSAttributedStringKey, id>* const kStatusDisabledAttributes = @{
|
||||
NSFontAttributeName : [NSFont messageFontOfSize:9.0],
|
||||
NSParagraphStyleAttributeName : sStatusParagraphStyle(),
|
||||
NSForegroundColorAttributeName : NSColor.disabledControlTextColor
|
||||
};
|
||||
|
||||
@implementation FileNameCell
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
NSMutableParagraphStyle* paragraphStyle = [NSParagraphStyle.defaultParagraphStyle mutableCopy];
|
||||
paragraphStyle.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
|
||||
_fTitleAttributes = [[NSMutableDictionary alloc]
|
||||
initWithObjectsAndKeys:[NSFont messageFontOfSize:12.0], NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil];
|
||||
|
||||
NSMutableParagraphStyle* statusParagraphStyle = [NSParagraphStyle.defaultParagraphStyle mutableCopy];
|
||||
statusParagraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
|
||||
_fStatusAttributes = [[NSMutableDictionary alloc]
|
||||
initWithObjectsAndKeys:[NSFont messageFontOfSize:9.0], NSFontAttributeName, statusParagraphStyle, NSParagraphStyleAttributeName, nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(NSZone*)zone
|
||||
{
|
||||
FileNameCell* copy = [super copyWithZone:zone];
|
||||
|
||||
copy->_fTitleAttributes = _fTitleAttributes;
|
||||
copy->_fStatusAttributes = _fStatusAttributes;
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
- (NSImage*)image
|
||||
{
|
||||
FileListNode* node = (FileListNode*)self.objectValue;
|
||||
@@ -87,40 +93,36 @@ static CGFloat const kPaddingExpansionFrame = 2.0;
|
||||
respectFlipped:YES
|
||||
hints:nil];
|
||||
|
||||
NSColor *titleColor, *statusColor;
|
||||
FileListNode* node = self.objectValue;
|
||||
AttributesStyle style;
|
||||
if (self.backgroundStyle == NSBackgroundStyleEmphasized)
|
||||
{
|
||||
titleColor = statusColor = NSColor.whiteColor;
|
||||
style = AttributesStyleEmphasized;
|
||||
}
|
||||
else if ([node.torrent checkForFiles:node.indexes] == NSControlStateValueOff)
|
||||
{
|
||||
titleColor = statusColor = NSColor.disabledControlTextColor;
|
||||
style = AttributesStyleDisabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
titleColor = NSColor.controlTextColor;
|
||||
statusColor = NSColor.secondaryLabelColor;
|
||||
style = AttributesStyleNormal;
|
||||
}
|
||||
|
||||
self.fTitleAttributes[NSForegroundColorAttributeName] = titleColor;
|
||||
self.fStatusAttributes[NSForegroundColorAttributeName] = statusColor;
|
||||
|
||||
//title
|
||||
NSAttributedString* titleString = self.attributedTitle;
|
||||
NSRect titleRect = [self rectForTitleWithString:titleString inBounds:cellFrame];
|
||||
NSAttributedString* titleString = [self attributedTitleWithStyle:style];
|
||||
NSRect titleRect = [self rectForTitleWithStringSize:[titleString size] inBounds:cellFrame];
|
||||
[titleString drawInRect:titleRect];
|
||||
|
||||
//status
|
||||
NSAttributedString* statusString = self.attributedStatus;
|
||||
NSAttributedString* statusString = [self attributedStatusWithStyle:style];
|
||||
NSRect statusRect = [self rectForStatusWithString:statusString withTitleRect:titleRect inBounds:cellFrame];
|
||||
[statusString drawInRect:statusRect];
|
||||
}
|
||||
|
||||
- (NSRect)expansionFrameWithFrame:(NSRect)cellFrame inView:(NSView*)view
|
||||
{
|
||||
NSAttributedString* titleString = self.attributedTitle;
|
||||
NSRect realRect = [self rectForTitleWithString:titleString inBounds:cellFrame];
|
||||
NSAttributedString* titleString = [self attributedTitleWithStyle:AttributesStyleNormal];
|
||||
NSRect realRect = [self rectForTitleWithStringSize:[titleString size] inBounds:cellFrame];
|
||||
|
||||
if ([titleString size].width > NSWidth(realRect) &&
|
||||
NSMouseInRect([view convertPoint:view.window.mouseLocationOutsideOfEventStream fromView:nil], realRect, view.flipped))
|
||||
@@ -137,16 +139,15 @@ static CGFloat const kPaddingExpansionFrame = 2.0;
|
||||
cellFrame.origin.x += kPaddingExpansionFrame;
|
||||
cellFrame.origin.y += kPaddingExpansionFrame;
|
||||
|
||||
self.fTitleAttributes[NSForegroundColorAttributeName] = NSColor.controlTextColor;
|
||||
NSAttributedString* titleString = self.attributedTitle;
|
||||
NSAttributedString* titleString = [self attributedTitleWithStyle:AttributesStyleNormal];
|
||||
[titleString drawInRect:cellFrame];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (NSRect)rectForTitleWithString:(NSAttributedString*)string inBounds:(NSRect)bounds
|
||||
- (NSRect)rectForTitleWithStringSize:(NSSize)stringSize inBounds:(NSRect)bounds
|
||||
{
|
||||
NSSize const titleSize = [string size];
|
||||
NSSize const titleSize = stringSize;
|
||||
|
||||
//no right padding, so that there's not too much space between this and the priority image
|
||||
NSRect result;
|
||||
@@ -189,13 +190,15 @@ static CGFloat const kPaddingExpansionFrame = 2.0;
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSAttributedString*)attributedTitle
|
||||
- (NSAttributedString*)attributedTitleWithStyle:(AttributesStyle)style
|
||||
{
|
||||
NSString* title = ((FileListNode*)self.objectValue).name;
|
||||
return [[NSAttributedString alloc] initWithString:title attributes:self.fTitleAttributes];
|
||||
return [[NSAttributedString alloc] initWithString:title attributes:style == AttributesStyleEmphasized ? kTitleEmphasizedAttributes :
|
||||
style == AttributesStyleDisabled ? kTitleDisabledAttributes :
|
||||
kTitleAttributes];
|
||||
}
|
||||
|
||||
- (NSAttributedString*)attributedStatus
|
||||
- (NSAttributedString*)attributedStatusWithStyle:(AttributesStyle)style
|
||||
{
|
||||
FileListNode* node = (FileListNode*)self.objectValue;
|
||||
Torrent* torrent = node.torrent;
|
||||
@@ -207,7 +210,9 @@ static CGFloat const kPaddingExpansionFrame = 2.0;
|
||||
percentString,
|
||||
[NSString stringForFileSize:node.size]];
|
||||
|
||||
return [[NSAttributedString alloc] initWithString:status attributes:self.fStatusAttributes];
|
||||
return [[NSAttributedString alloc] initWithString:status attributes:style == AttributesStyleEmphasized ? kStatusEmphasizedAttributes :
|
||||
style == AttributesStyleDisabled ? kStatusDisabledAttributes :
|
||||
kStatusAttributes];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 261 B |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 897 B After Width: | Height: | Size: 841 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 455 B After Width: | Height: | Size: 339 B |
|
Before Width: | Height: | Size: 966 B After Width: | Height: | Size: 723 B |
|
Before Width: | Height: | Size: 298 B After Width: | Height: | Size: 276 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 345 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 616 B |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 177 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 341 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 188 B After Width: | Height: | Size: 187 B |
|
Before Width: | Height: | Size: 353 B After Width: | Height: | Size: 349 B |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
BIN
macosx/Images/Images.xcassets/Lock.imageset/Lock.png
vendored
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 514 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 811 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 133 B After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 133 B After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 245 B After Width: | Height: | Size: 244 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 285 B After Width: | Height: | Size: 282 B |
|
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 283 B |
|
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 289 B |
|
Before Width: | Height: | Size: 479 B After Width: | Height: | Size: 478 B |
|
Before Width: | Height: | Size: 266 B After Width: | Height: | Size: 257 B |
|
Before Width: | Height: | Size: 77 B After Width: | Height: | Size: 76 B |
|
Before Width: | Height: | Size: 99 B After Width: | Height: | Size: 87 B |
|
Before Width: | Height: | Size: 285 B After Width: | Height: | Size: 276 B |
|
Before Width: | Height: | Size: 335 B After Width: | Height: | Size: 299 B |
|
Before Width: | Height: | Size: 621 B After Width: | Height: | Size: 609 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 345 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 616 B |
@@ -428,8 +428,7 @@ static NSString* const kWebSeedAnimationId = @"webSeed";
|
||||
{
|
||||
portString = NSLocalizedString(@"N/A", "Inspector -> Peers tab -> table row tooltip");
|
||||
}
|
||||
[components addObject:[NSString stringWithFormat:@"%@: %@",
|
||||
NSLocalizedString(@"Port", "Inspector -> Peers tab -> table row tooltip"),
|
||||
[components addObject:[NSString stringWithFormat:NSLocalizedString(@"Port: %@", "Inspector -> Peers tab -> table row tooltip"),
|
||||
portString]];
|
||||
|
||||
NSInteger const peerFrom = [peer[@"From"] integerValue];
|
||||
|
||||
@@ -129,7 +129,7 @@ typedef struct PieceInfo
|
||||
self.image = [[NSImage alloc] initWithSize:self.bounds.size];
|
||||
|
||||
[self clearView];
|
||||
[self setNeedsDisplay];
|
||||
self.needsDisplay = YES;
|
||||
}
|
||||
|
||||
- (void)clearView
|
||||
@@ -196,7 +196,7 @@ typedef struct PieceInfo
|
||||
NSRectFillListWithColors(cFillRects, cFillColors, numCells);
|
||||
return YES;
|
||||
}];
|
||||
[self setNeedsDisplay];
|
||||
self.needsDisplay = YES;
|
||||
}
|
||||
|
||||
// save the current state so we can compare it later
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// License text can be found in the licenses/ folder.
|
||||
|
||||
#import "TorrentCell.h"
|
||||
#import "FileNameCell.h"
|
||||
#import "GroupsController.h"
|
||||
#import "NSImageAdditions.h"
|
||||
#import "NSStringAdditions.h"
|
||||
@@ -48,13 +49,35 @@ static CGFloat const kPiecesTotalPercent = 0.6;
|
||||
|
||||
static NSInteger const kMaxPieces = 18 * 18;
|
||||
|
||||
static NSMutableParagraphStyle* sParagraphStyle()
|
||||
{
|
||||
NSMutableParagraphStyle* paragraphStyle = [NSParagraphStyle.defaultParagraphStyle mutableCopy];
|
||||
paragraphStyle.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
return paragraphStyle;
|
||||
}
|
||||
static NSDictionary<NSAttributedStringKey, id>* const kTitleAttributes = @{
|
||||
NSFontAttributeName : [NSFont messageFontOfSize:12.0],
|
||||
NSParagraphStyleAttributeName : sParagraphStyle(),
|
||||
NSForegroundColorAttributeName : NSColor.labelColor
|
||||
};
|
||||
static NSDictionary<NSAttributedStringKey, id>* const kStatusAttributes = @{
|
||||
NSFontAttributeName : [NSFont messageFontOfSize:10.0],
|
||||
NSParagraphStyleAttributeName : sParagraphStyle(),
|
||||
NSForegroundColorAttributeName : NSColor.secondaryLabelColor
|
||||
};
|
||||
static NSDictionary<NSAttributedStringKey, id>* const kTitleEmphasizedAttributes = @{
|
||||
NSFontAttributeName : [NSFont messageFontOfSize:12.0],
|
||||
NSParagraphStyleAttributeName : sParagraphStyle(),
|
||||
NSForegroundColorAttributeName : NSColor.whiteColor
|
||||
};
|
||||
static NSDictionary<NSAttributedStringKey, id>* const kStatusEmphasizedAttributes = @{
|
||||
NSFontAttributeName : [NSFont messageFontOfSize:10.0],
|
||||
NSParagraphStyleAttributeName : sParagraphStyle(),
|
||||
NSForegroundColorAttributeName : NSColor.whiteColor
|
||||
};
|
||||
|
||||
@interface TorrentCell ()
|
||||
|
||||
@property(nonatomic, readonly) NSUserDefaults* fDefaults;
|
||||
|
||||
@property(nonatomic, readonly) NSMutableDictionary* fTitleAttributes;
|
||||
@property(nonatomic, readonly) NSMutableDictionary* fStatusAttributes;
|
||||
|
||||
@property(nonatomic) BOOL fTracking;
|
||||
@property(nonatomic) BOOL fMouseDownControlButton;
|
||||
@property(nonatomic) BOOL fMouseDownRevealButton;
|
||||
@@ -63,13 +86,6 @@ static NSInteger const kMaxPieces = 18 * 18;
|
||||
@property(nonatomic, readonly) NSColor* fBluePieceColor;
|
||||
@property(nonatomic, readonly) NSColor* fBarMinimalBorderColor;
|
||||
|
||||
@property(nonatomic, readonly) NSAttributedString* attributedTitle;
|
||||
- (NSAttributedString*)attributedStatusString:(NSString*)string;
|
||||
|
||||
@property(nonatomic, readonly) NSString* buttonString;
|
||||
@property(nonatomic, readonly) NSString* statusString;
|
||||
@property(nonatomic, readonly) NSString* minimalStatusString;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TorrentCell
|
||||
@@ -79,17 +95,6 @@ static NSInteger const kMaxPieces = 18 * 18;
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
NSMutableParagraphStyle* paragraphStyle = [NSParagraphStyle.defaultParagraphStyle mutableCopy];
|
||||
paragraphStyle.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
|
||||
_fTitleAttributes = [[NSMutableDictionary alloc] initWithCapacity:3];
|
||||
_fTitleAttributes[NSFontAttributeName] = [NSFont messageFontOfSize:12.0];
|
||||
_fTitleAttributes[NSParagraphStyleAttributeName] = paragraphStyle;
|
||||
|
||||
_fStatusAttributes = [[NSMutableDictionary alloc] initWithCapacity:3];
|
||||
_fStatusAttributes[NSFontAttributeName] = [NSFont messageFontOfSize:10.0];
|
||||
_fStatusAttributes[NSParagraphStyleAttributeName] = paragraphStyle;
|
||||
|
||||
_fBluePieceColor = [NSColor colorWithCalibratedRed:0.0 green:0.4 blue:0.8 alpha:1.0];
|
||||
_fBarBorderColor = [NSColor colorWithCalibratedWhite:0.0 alpha:0.2];
|
||||
_fBarMinimalBorderColor = [NSColor colorWithCalibratedWhite:0.0 alpha:0.015];
|
||||
@@ -100,8 +105,6 @@ static NSInteger const kMaxPieces = 18 * 18;
|
||||
- (id)copyWithZone:(NSZone*)zone
|
||||
{
|
||||
TorrentCell* copy = [super copyWithZone:zone];
|
||||
copy->_fTitleAttributes = [_fTitleAttributes mutableCopyWithZone:zone];
|
||||
copy->_fStatusAttributes = [_fStatusAttributes mutableCopyWithZone:zone];
|
||||
copy->_fBluePieceColor = _fBluePieceColor;
|
||||
copy->_fBarBorderColor = _fBarBorderColor;
|
||||
copy->_fBarMinimalBorderColor = _fBarMinimalBorderColor;
|
||||
@@ -333,27 +336,14 @@ static NSInteger const kMaxPieces = 18 * 18;
|
||||
hints:nil];
|
||||
}
|
||||
|
||||
//text color
|
||||
NSColor *titleColor, *statusColor;
|
||||
if (self.backgroundStyle == NSBackgroundStyleEmphasized)
|
||||
{
|
||||
titleColor = statusColor = NSColor.whiteColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
titleColor = NSColor.labelColor;
|
||||
statusColor = NSColor.secondaryLabelColor;
|
||||
}
|
||||
|
||||
self.fTitleAttributes[NSForegroundColorAttributeName] = titleColor;
|
||||
self.fStatusAttributes[NSForegroundColorAttributeName] = statusColor;
|
||||
AttributesStyle style = self.backgroundStyle == NSBackgroundStyleEmphasized ? AttributesStyleEmphasized : AttributesStyleNormal;
|
||||
|
||||
CGFloat titleRightBound;
|
||||
//minimal status
|
||||
if (minimal)
|
||||
{
|
||||
NSAttributedString* minimalString = [self attributedStatusString:self.minimalStatusString];
|
||||
NSRect minimalStatusRect = [self rectForMinimalStatusWithString:minimalString inBounds:cellFrame];
|
||||
NSAttributedString* minimalString = [self attributedStatusString:self.minimalStatusString style:style];
|
||||
NSRect minimalStatusRect = [self rectForMinimalStatusWithStringSize:[minimalString size] inBounds:cellFrame];
|
||||
|
||||
if (!self.hover)
|
||||
{
|
||||
@@ -365,7 +355,7 @@ static NSInteger const kMaxPieces = 18 * 18;
|
||||
//progress
|
||||
else
|
||||
{
|
||||
NSAttributedString* progressString = [self attributedStatusString:torrent.progressString];
|
||||
NSAttributedString* progressString = [self attributedStatusString:torrent.progressString style:style];
|
||||
NSRect progressRect = [self rectForProgressWithStringInBounds:cellFrame];
|
||||
|
||||
[progressString drawInRect:progressRect];
|
||||
@@ -455,8 +445,9 @@ static NSInteger const kMaxPieces = 18 * 18;
|
||||
}
|
||||
|
||||
//title
|
||||
NSAttributedString* titleString = self.attributedTitle;
|
||||
NSRect titleRect = [self rectForTitleWithString:titleString withRightBound:titleRightBound inBounds:cellFrame minimal:minimal];
|
||||
NSAttributedString* titleString = [self attributedTitleWithStyle:style];
|
||||
NSRect titleRect = [self rectForTitleWithStringSize:[titleString size] withRightBound:titleRightBound inBounds:cellFrame
|
||||
minimal:minimal];
|
||||
[titleString drawInRect:titleRect];
|
||||
|
||||
//priority icon
|
||||
@@ -480,7 +471,7 @@ static NSInteger const kMaxPieces = 18 * 18;
|
||||
//status
|
||||
if (!minimal)
|
||||
{
|
||||
NSAttributedString* statusString = [self attributedStatusString:self.statusString];
|
||||
NSAttributedString* statusString = [self attributedStatusString:self.statusString style:style];
|
||||
[statusString drawInRect:[self rectForStatusWithStringInBounds:cellFrame]];
|
||||
}
|
||||
}
|
||||
@@ -493,8 +484,8 @@ static NSInteger const kMaxPieces = 18 * 18;
|
||||
CGFloat titleRightBound;
|
||||
if (minimal)
|
||||
{
|
||||
NSAttributedString* minimalString = [self attributedStatusString:self.minimalStatusString];
|
||||
NSRect minimalStatusRect = [self rectForMinimalStatusWithString:minimalString inBounds:cellFrame];
|
||||
NSAttributedString* minimalString = [self attributedStatusString:self.minimalStatusString style:AttributesStyleNormal];
|
||||
NSRect minimalStatusRect = [self rectForMinimalStatusWithStringSize:[minimalString size] inBounds:cellFrame];
|
||||
|
||||
titleRightBound = NSMinX(minimalStatusRect);
|
||||
|
||||
@@ -509,15 +500,17 @@ static NSInteger const kMaxPieces = 18 * 18;
|
||||
titleRightBound = NSMaxX(cellFrame);
|
||||
}
|
||||
|
||||
NSAttributedString* titleString = self.attributedTitle;
|
||||
NSRect realRect = [self rectForTitleWithString:titleString withRightBound:titleRightBound inBounds:cellFrame minimal:minimal];
|
||||
NSAttributedString* titleString = [self attributedTitleWithStyle:AttributesStyleNormal];
|
||||
NSSize titleStringSize = [titleString size];
|
||||
NSRect realRect = [self rectForTitleWithStringSize:titleStringSize withRightBound:titleRightBound inBounds:cellFrame
|
||||
minimal:minimal];
|
||||
|
||||
NSAssert([titleString size].width >= NSWidth(realRect), @"Full rect width should not be less than the used title rect width!");
|
||||
NSAssert(titleStringSize.width >= NSWidth(realRect), @"Full rect width should not be less than the used title rect width!");
|
||||
|
||||
if ([titleString size].width > NSWidth(realRect) &&
|
||||
if (titleStringSize.width > NSWidth(realRect) &&
|
||||
NSMouseInRect([view convertPoint:view.window.mouseLocationOutsideOfEventStream fromView:nil], realRect, view.flipped))
|
||||
{
|
||||
realRect.size.width = [titleString size].width;
|
||||
realRect.size.width = titleStringSize.width;
|
||||
return NSInsetRect(realRect, -kPaddingExpansionFrame, -kPaddingExpansionFrame);
|
||||
}
|
||||
|
||||
@@ -529,8 +522,7 @@ static NSInteger const kMaxPieces = 18 * 18;
|
||||
cellFrame.origin.x += kPaddingExpansionFrame;
|
||||
cellFrame.origin.y += kPaddingExpansionFrame;
|
||||
|
||||
self.fTitleAttributes[NSForegroundColorAttributeName] = NSColor.labelColor;
|
||||
NSAttributedString* titleString = self.attributedTitle;
|
||||
NSAttributedString* titleString = [self attributedTitleWithStyle:AttributesStyleNormal];
|
||||
[titleString drawInRect:cellFrame];
|
||||
}
|
||||
|
||||
@@ -711,10 +703,10 @@ static NSInteger const kMaxPieces = 18 * 18;
|
||||
hints:nil];
|
||||
}
|
||||
|
||||
- (NSRect)rectForMinimalStatusWithString:(NSAttributedString*)string inBounds:(NSRect)bounds
|
||||
- (NSRect)rectForMinimalStatusWithStringSize:(NSSize)stringSize inBounds:(NSRect)bounds
|
||||
{
|
||||
NSRect result;
|
||||
result.size = [string size];
|
||||
result.size = stringSize;
|
||||
|
||||
result.origin.x = NSMaxX(bounds) - (kPaddingHorizontal + NSWidth(result) + kPaddingEdgeMax);
|
||||
result.origin.y = ceil(NSMidY(bounds) - NSHeight(result) * 0.5);
|
||||
@@ -722,10 +714,10 @@ static NSInteger const kMaxPieces = 18 * 18;
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSRect)rectForTitleWithString:(NSAttributedString*)string
|
||||
withRightBound:(CGFloat)rightBound
|
||||
inBounds:(NSRect)bounds
|
||||
minimal:(BOOL)minimal
|
||||
- (NSRect)rectForTitleWithStringSize:(NSSize)stringSize
|
||||
withRightBound:(CGFloat)rightBound
|
||||
inBounds:(NSRect)bounds
|
||||
minimal:(BOOL)minimal
|
||||
{
|
||||
NSRect result;
|
||||
result.origin.x = NSMinX(bounds) + kPaddingHorizontal + (minimal ? kImageSizeMin : kImageSizeRegular) + kPaddingBetweenImageAndTitle;
|
||||
@@ -748,7 +740,7 @@ static NSInteger const kMaxPieces = 18 * 18;
|
||||
{
|
||||
result.size.width -= kPriorityIconSize + kPaddingBetweenTitleAndPriority;
|
||||
}
|
||||
result.size.width = MIN(NSWidth(result), [string size].width);
|
||||
result.size.width = MIN(NSWidth(result), stringSize.width);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -857,15 +849,17 @@ static NSInteger const kMaxPieces = 18 * 18;
|
||||
return NSMakeRect(NSMinX(bounds) - padding * 0.5, NSMidY(bounds) - imageSize * 0.5, imageSize, imageSize);
|
||||
}
|
||||
|
||||
- (NSAttributedString*)attributedTitle
|
||||
- (NSAttributedString*)attributedTitleWithStyle:(AttributesStyle)style
|
||||
{
|
||||
NSString* title = ((Torrent*)self.representedObject).name;
|
||||
return [[NSAttributedString alloc] initWithString:title attributes:self.fTitleAttributes];
|
||||
return [[NSAttributedString alloc] initWithString:title
|
||||
attributes:style == AttributesStyleEmphasized ? kTitleEmphasizedAttributes : kTitleAttributes];
|
||||
}
|
||||
|
||||
- (NSAttributedString*)attributedStatusString:(NSString*)string
|
||||
- (NSAttributedString*)attributedStatusString:(NSString*)string style:(AttributesStyle)style
|
||||
{
|
||||
return [[NSAttributedString alloc] initWithString:string attributes:self.fStatusAttributes];
|
||||
return [[NSAttributedString alloc] initWithString:string
|
||||
attributes:style == AttributesStyleEmphasized ? kStatusEmphasizedAttributes : kStatusAttributes];
|
||||
}
|
||||
|
||||
- (NSString*)buttonString
|
||||
|
||||
@@ -126,7 +126,13 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
|
||||
//disable highlight color and set manually in drawRow
|
||||
[self setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleNone];
|
||||
|
||||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(setNeedsDisplay) name:@"RefreshTorrentTable" object:nil];
|
||||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(refreshTorrentTable) name:@"RefreshTorrentTable"
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)refreshTorrentTable
|
||||
{
|
||||
self.needsDisplay = YES;
|
||||
}
|
||||
|
||||
- (BOOL)isGroupCollapsed:(NSInteger)value
|
||||
|
||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.1 KiB |
@@ -674,7 +674,7 @@
|
||||
"Peers" = "Modparter";
|
||||
|
||||
/* Inspector -> Peers tab -> table row tooltip */
|
||||
"Port" = "Port";
|
||||
"Port: %@" = "Port: %@";
|
||||
|
||||
/* Preferences -> Network -> port status */
|
||||
"Port check site is down" = "Port checksite er nede";
|
||||
|
||||
@@ -674,7 +674,7 @@
|
||||
"Peers" = "Teilnehmer";
|
||||
|
||||
/* Inspector -> Peers tab -> table row tooltip */
|
||||
"Port" = "Port";
|
||||
"Port: %@" = "Port: %@";
|
||||
|
||||
/* Preferences -> Network -> port status */
|
||||
"Port check site is down" = "Site zum Überprüfen des Ports ist nicht erreichbar.";
|
||||
|
||||
@@ -674,7 +674,7 @@
|
||||
"Peers" = "Peers";
|
||||
|
||||
/* Inspector -> Peers tab -> table row tooltip */
|
||||
"Port" = "Port";
|
||||
"Port: %@" = "Port: %@";
|
||||
|
||||
/* Preferences -> Network -> port status */
|
||||
"Port check site is down" = "Port check site is down";
|
||||
|
||||
@@ -674,7 +674,7 @@
|
||||
"Peers" = "Clientes";
|
||||
|
||||
/* Inspector -> Peers tab -> table row tooltip */
|
||||
"Port" = "Puerto";
|
||||
"Port: %@" = "Puerto: %@";
|
||||
|
||||
/* Preferences -> Network -> port status */
|
||||
"Port check site is down" = "Sitio de comprobación de puertos caido";
|
||||
|
||||