// This file Copyright © 2022-2023 Mnemosyne LLC. // It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), // or any future license endorsed by Mnemosyne LLC. // License text can be found in the licenses/ folder. #include "Torrent.h" #include "DynamicPropertyStore.h" #include "IconCache.h" #include "Percents.h" #include "Utils.h" #include #include #include #include #include #include #include #include using namespace std::string_view_literals; namespace { template Glib::Value& column_value_cast(Glib::ValueBase& value, Gtk::TreeModelColumn const& /*column*/) { return static_cast&>(value); } template>> void update_cache_value(T& value, U&& new_value, Torrent::ChangeFlags& changes, Torrent::ChangeFlag flag) { using std::rel_ops::operator!=; if (value != new_value) { value = std::forward(new_value); changes.set(flag); } } template>> void update_cache_value(T& value, U new_value, T epsilon, Torrent::ChangeFlags& changes, Torrent::ChangeFlag flag) { if (std::fabs(value - new_value) >= epsilon) { value = new_value; changes.set(flag); } } unsigned int build_torrent_trackers_hash(tr_torrent const& torrent) { auto hash = uint64_t(0); for (auto i = size_t(0), n = tr_torrentTrackerCount(&torrent); i < n; ++i) { for (auto const ch : std::string_view{ tr_torrentTracker(&torrent, i).announce }) { hash = (hash << 4U) ^ (hash >> 28U) ^ static_cast(ch); } } return hash; } std::string_view get_mime_type(tr_torrent const& torrent) { auto const n_files = tr_torrentFileCount(&torrent); if (n_files == 0) { return UnknownMimeType; } if (n_files > 1) { return DirectoryMimeType; } auto const name = std::string_view(tr_torrentFile(&torrent, 0).name); return name.find('/') != std::string_view::npos ? DirectoryMimeType : tr_get_mime_type_for_filename(name); } std::string_view get_activity_direction(tr_torrent_activity activity) { switch (activity) { case TR_STATUS_DOWNLOAD: return "down"sv; case TR_STATUS_SEED: return "up"sv; default: return "idle"sv; } } } // namespace Torrent::Columns::Columns() { add(self); add(name_collated); } class Torrent::Impl { public: enum class Property : guint { ICON = 1, NAME, PERCENT_DONE, SHORT_STATUS, LONG_PROGRESS, LONG_STATUS, SENSITIVE, CSS_CLASSES, N_PROPS }; using PropertyStore = DynamicPropertyStore; struct Cache { Glib::ustring error_message; Glib::ustring name; Glib::ustring name_collated; std::string_view mime_type; uint64_t have_unchecked = {}; uint64_t have_valid = {}; uint64_t left_until_done = {}; uint64_t size_when_done = {}; uint64_t total_size = {}; uint64_t uploaded_ever = {}; size_t queue_position = {}; time_t added_date = {}; time_t eta = {}; tr_torrent_activity activity = {}; unsigned int trackers = {}; int active_peer_count = {}; int active_peers_down = {}; int active_peers_up = {}; int error_code = {}; Percents activity_percent_done = {}; Percents metadata_percent_complete = {}; Percents percent_complete = {}; Percents percent_done = {}; Percents recheck_progress = {}; Percents seed_ratio_percent_done = {}; uint16_t peers_connected = {}; uint16_t peers_getting_from_us = {}; uint16_t peers_sending_to_us = {}; uint16_t webseeds_sending_to_us = {}; float ratio = {}; float seed_ratio = {}; float speed_down = {}; float speed_up = {}; tr_priority_t priority = {}; bool active = {}; bool finished = {}; bool has_metadata = {}; bool has_seed_ratio = {}; bool stalled = {}; }; public: Impl(Torrent& torrent, tr_torrent* raw_torrent); tr_torrent* get_raw_torrent() { return raw_torrent_; } Cache& get_cache() { return cache_; } ChangeFlags update_cache(); void notify_property_changes(ChangeFlags changes) const; void get_value(int column, Glib::ValueBase& value) const; [[nodiscard]] Glib::RefPtr get_icon() const; [[nodiscard]] Glib::ustring get_short_status_text() const; [[nodiscard]] Glib::ustring get_long_progress_text() const; [[nodiscard]] Glib::ustring get_long_status_text() const; [[nodiscard]] std::vector get_css_classes() const; static void class_init(void* cls, void* user_data); private: [[nodiscard]] Glib::ustring get_short_transfer_text() const; [[nodiscard]] Glib::ustring get_error_text() const; [[nodiscard]] Glib::ustring get_activity_text() const; private: Torrent& torrent_; tr_torrent* const raw_torrent_; Cache cache_; }; Torrent::Impl::Impl(Torrent& torrent, tr_torrent* raw_torrent) : torrent_(torrent) , raw_torrent_(raw_torrent) { if (raw_torrent_ != nullptr) { update_cache(); } } Torrent::ChangeFlags Torrent::Impl::update_cache() { auto result = ChangeFlags(); auto const* const stats = tr_torrentStat(raw_torrent_); g_return_val_if_fail(stats != nullptr, Torrent::ChangeFlags()); auto seed_ratio = 0.0; auto const has_seed_ratio = tr_torrentGetSeedRatio(raw_torrent_, &seed_ratio); update_cache_value(cache_.name, tr_torrentName(raw_torrent_), result, ChangeFlag::NAME); update_cache_value(cache_.speed_up, stats->pieceUploadSpeed_KBps, 0.01F, result, ChangeFlag::SPEED_UP); update_cache_value(cache_.speed_down, stats->pieceDownloadSpeed_KBps, 0.01F, result, ChangeFlag::SPEED_DOWN); update_cache_value(cache_.active_peers_up, stats->peersGettingFromUs, result, ChangeFlag::ACTIVE_PEERS_UP); update_cache_value( cache_.active_peers_down, stats->peersSendingToUs + stats->webseedsSendingToUs, result, ChangeFlag::ACTIVE_PEERS_DOWN); update_cache_value(cache_.recheck_progress, Percents(stats->recheckProgress), result, ChangeFlag::RECHECK_PROGRESS); update_cache_value( cache_.active, stats->peersSendingToUs > 0 || stats->peersGettingFromUs > 0 || stats->activity == TR_STATUS_CHECK, result, ChangeFlag::ACTIVE); update_cache_value(cache_.activity, stats->activity, result, ChangeFlag::ACTIVITY); update_cache_value( cache_.activity_percent_done, Percents(std::clamp( stats->activity == TR_STATUS_SEED && has_seed_ratio ? stats->seedRatioPercentDone : stats->percentDone, 0.0F, 1.0F)), result, ChangeFlag::PERCENT_DONE); update_cache_value(cache_.finished, stats->finished, result, ChangeFlag::FINISHED); update_cache_value(cache_.priority, tr_torrentGetPriority(raw_torrent_), result, ChangeFlag::PRIORITY); update_cache_value(cache_.queue_position, stats->queuePosition, result, ChangeFlag::QUEUE_POSITION); update_cache_value(cache_.trackers, build_torrent_trackers_hash(*raw_torrent_), result, ChangeFlag::TRACKERS); update_cache_value(cache_.error_code, stats->error, result, ChangeFlag::ERROR_CODE); update_cache_value(cache_.error_message, stats->errorString, result, ChangeFlag::ERROR_MESSAGE); update_cache_value( cache_.active_peer_count, stats->peersSendingToUs + stats->peersGettingFromUs + stats->webseedsSendingToUs, result, ChangeFlag::ACTIVE_PEER_COUNT); update_cache_value(cache_.mime_type, get_mime_type(*raw_torrent_), result, ChangeFlag::MIME_TYPE); update_cache_value(cache_.has_metadata, tr_torrentHasMetadata(raw_torrent_), result, ChangeFlag::HAS_METADATA); update_cache_value(cache_.stalled, stats->isStalled, result, ChangeFlag::STALLED); update_cache_value(cache_.ratio, stats->ratio, 0.01F, result, ChangeFlag::RATIO); update_cache_value(cache_.added_date, stats->addedDate, result, ChangeFlag::ADDED_DATE); update_cache_value(cache_.eta, stats->eta, result, ChangeFlag::ETA); update_cache_value(cache_.percent_complete, Percents(stats->percentComplete), result, ChangeFlag::PERCENT_COMPLETE); update_cache_value( cache_.seed_ratio_percent_done, Percents(stats->seedRatioPercentDone), result, ChangeFlag::SEED_RATIO_PERCENT_DONE); update_cache_value(cache_.total_size, tr_torrentTotalSize(raw_torrent_), result, ChangeFlag::TOTAL_SIZE); update_cache_value(cache_.has_seed_ratio, has_seed_ratio, result, ChangeFlag::LONG_PROGRESS); update_cache_value(cache_.have_unchecked, stats->haveUnchecked, result, ChangeFlag::LONG_PROGRESS); update_cache_value(cache_.have_valid, stats->haveValid, result, ChangeFlag::LONG_PROGRESS); update_cache_value(cache_.left_until_done, stats->leftUntilDone, result, ChangeFlag::LONG_PROGRESS); update_cache_value(cache_.percent_done, Percents(stats->percentDone), result, ChangeFlag::LONG_PROGRESS); update_cache_value(cache_.seed_ratio, static_cast(seed_ratio), 0.01F, result, ChangeFlag::LONG_PROGRESS); update_cache_value(cache_.size_when_done, stats->sizeWhenDone, result, ChangeFlag::LONG_PROGRESS); update_cache_value(cache_.uploaded_ever, stats->uploadedEver, result, ChangeFlag::LONG_PROGRESS); update_cache_value( cache_.metadata_percent_complete, Percents(stats->metadataPercentComplete), result, ChangeFlag::LONG_STATUS); update_cache_value(cache_.peers_connected, stats->peersConnected, result, ChangeFlag::LONG_STATUS); update_cache_value(cache_.peers_getting_from_us, stats->peersGettingFromUs, result, ChangeFlag::LONG_STATUS); update_cache_value(cache_.peers_sending_to_us, stats->peersSendingToUs, result, ChangeFlag::LONG_STATUS); update_cache_value(cache_.webseeds_sending_to_us, stats->webseedsSendingToUs, result, ChangeFlag::LONG_STATUS); if (result.test(ChangeFlag::NAME)) { cache_.name_collated = fmt::format("{}\t{}", cache_.name.lowercase(), tr_torrentView(raw_torrent_).hash_string); } return result; } void Torrent::Impl::notify_property_changes(ChangeFlags changes) const { // Updating the model triggers off resort/refresh, so don't notify unless something's actually changed if (changes.none()) { return; } #if GTKMM_CHECK_VERSION(4, 0, 0) static auto constexpr properties_flags = std::array, PropertyStore::PropertyCount - 1>({ { { Property::ICON, ChangeFlag::MIME_TYPE }, { Property::NAME, ChangeFlag::NAME }, { Property::PERCENT_DONE, ChangeFlag::PERCENT_DONE }, { Property::SHORT_STATUS, ChangeFlag::ACTIVE_PEERS_DOWN | ChangeFlag::ACTIVE_PEERS_UP | ChangeFlag::ACTIVITY | ChangeFlag::FINISHED | ChangeFlag::RATIO | ChangeFlag::RECHECK_PROGRESS | ChangeFlag::SPEED_DOWN | ChangeFlag::SPEED_UP }, { Property::LONG_PROGRESS, ChangeFlag::ACTIVITY | ChangeFlag::ETA | ChangeFlag::LONG_PROGRESS | ChangeFlag::PERCENT_COMPLETE | ChangeFlag::PERCENT_DONE | ChangeFlag::RATIO | ChangeFlag::TOTAL_SIZE }, { Property::LONG_STATUS, ChangeFlag::ACTIVE_PEERS_DOWN | ChangeFlag::ACTIVE_PEERS_UP | ChangeFlag::ACTIVITY | ChangeFlag::ERROR_CODE | ChangeFlag::ERROR_MESSAGE | ChangeFlag::HAS_METADATA | ChangeFlag::LONG_STATUS | ChangeFlag::SPEED_DOWN | ChangeFlag::SPEED_UP | ChangeFlag::STALLED }, { Property::SENSITIVE, ChangeFlag::ACTIVITY }, { Property::CSS_CLASSES, ChangeFlag::ACTIVITY | ChangeFlag::ERROR_CODE }, } }); auto& properties = PropertyStore::get(); for (auto const& [property, flags] : properties_flags) { if (changes.test(flags)) { properties.notify_changed(torrent_, property); } } #else // Reduce redraws by emitting non-detailed signal once for all changes gtr_object_notify_emit(torrent_); #endif } void Torrent::Impl::get_value(int column, Glib::ValueBase& value) const { static auto const& columns = get_columns(); if (column == columns.self.index()) { column_value_cast(value, columns.self).set(&torrent_); } else if (column == columns.name_collated.index()) { column_value_cast(value, columns.name_collated).set(cache_.name_collated); } } Glib::RefPtr Torrent::Impl::get_icon() const { return gtr_get_mime_type_icon(cache_.mime_type); } Glib::ustring Torrent::Impl::get_short_status_text() const { switch (cache_.activity) { case TR_STATUS_STOPPED: return cache_.finished ? _("Finished") : _("Paused"); case TR_STATUS_CHECK_WAIT: return _("Queued for verification"); case TR_STATUS_DOWNLOAD_WAIT: return _("Queued for download"); case TR_STATUS_SEED_WAIT: return _("Queued for seeding"); case TR_STATUS_CHECK: return fmt::format( // xgettext:no-c-format _("Verifying local data ({percent_done}% tested)"), fmt::arg("percent_done", cache_.recheck_progress.to_string())); case TR_STATUS_DOWNLOAD: case TR_STATUS_SEED: return fmt::format( FMT_STRING("{:s} {:s}"), get_short_transfer_text(), fmt::format(_("Ratio: {ratio}"), fmt::arg("ratio", tr_strlratio(cache_.ratio)))); default: return {}; } } Glib::ustring Torrent::Impl::get_long_progress_text() const { Glib::ustring gstr; bool const isDone = cache_.left_until_done == 0; auto const haveTotal = cache_.have_unchecked + cache_.have_valid; bool const isSeed = cache_.have_valid >= cache_.total_size; if (!isDone) // downloading { // 50 MB of 200 MB (25%) gstr += fmt::format( _("{current_size} of {complete_size} ({percent_done}%)"), fmt::arg("current_size", tr_strlsize(haveTotal)), fmt::arg("complete_size", tr_strlsize(cache_.size_when_done)), fmt::arg("percent_done", cache_.percent_done.to_string())); } else if (!isSeed && cache_.has_seed_ratio) // partial seed, seed ratio { // 50 MB of 200 MB (25%), uploaded 30 MB (Ratio: X%, Goal: Y%) gstr += fmt::format( // xgettext:no-c-format _("{current_size} of {complete_size} ({percent_complete}%), uploaded {uploaded_size} (Ratio: {ratio}, Goal: {seed_ratio})"), fmt::arg("current_size", tr_strlsize(haveTotal)), fmt::arg("complete_size", tr_strlsize(cache_.total_size)), fmt::arg("percent_complete", cache_.percent_complete.to_string()), fmt::arg("uploaded_size", tr_strlsize(cache_.uploaded_ever)), fmt::arg("ratio", tr_strlratio(cache_.ratio)), fmt::arg("seed_ratio", tr_strlratio(cache_.seed_ratio))); } else if (!isSeed) // partial seed, no seed ratio { gstr += fmt::format( // xgettext:no-c-format _("{current_size} of {complete_size} ({percent_complete}%), uploaded {uploaded_size} (Ratio: {ratio})"), fmt::arg("current_size", tr_strlsize(haveTotal)), fmt::arg("complete_size", tr_strlsize(cache_.total_size)), fmt::arg("percent_complete", cache_.percent_complete.to_string()), fmt::arg("uploaded_size", tr_strlsize(cache_.uploaded_ever)), fmt::arg("ratio", tr_strlratio(cache_.ratio))); } else if (cache_.has_seed_ratio) // seed, seed ratio { gstr += fmt::format( _("{complete_size}, uploaded {uploaded_size} (Ratio: {ratio}, Goal: {seed_ratio})"), fmt::arg("complete_size", tr_strlsize(cache_.total_size)), fmt::arg("uploaded_size", tr_strlsize(cache_.uploaded_ever)), fmt::arg("ratio", tr_strlratio(cache_.ratio)), fmt::arg("seed_ratio", tr_strlratio(cache_.seed_ratio))); } else // seed, no seed ratio { gstr += fmt::format( _("{complete_size}, uploaded {uploaded_size} (Ratio: {ratio})"), fmt::arg("complete_size", tr_strlsize(cache_.total_size)), fmt::arg("uploaded_size", tr_strlsize(cache_.uploaded_ever)), fmt::arg("ratio", tr_strlratio(cache_.ratio))); } // add time remaining when applicable if (cache_.activity == TR_STATUS_DOWNLOAD || (cache_.has_seed_ratio && cache_.activity == TR_STATUS_SEED)) { gstr += " - "; if (cache_.eta < 0) { gstr += _("Remaining time unknown"); } else { gstr += tr_format_time_left(cache_.eta); } } return gstr; } Glib::ustring Torrent::Impl::get_long_status_text() const { auto status_str = get_error_text(); if (status_str.empty()) { status_str = get_activity_text(); } switch (cache_.activity) { case TR_STATUS_CHECK_WAIT: case TR_STATUS_CHECK: case TR_STATUS_DOWNLOAD_WAIT: case TR_STATUS_SEED_WAIT: case TR_STATUS_STOPPED: break; default: if (auto const buf = get_short_transfer_text(); !std::empty(buf)) { status_str += fmt::format(FMT_STRING(" - {:s}"), buf); } } return status_str; } std::vector Torrent::Impl::get_css_classes() const { auto result = std::vector({ fmt::format("tr-transfer-{}", get_activity_direction(cache_.activity)), }); if (cache_.error_code != 0) { result.emplace_back("tr-error"); } return result; } void Torrent::Impl::class_init(void* cls, void* /*user_data*/) { PropertyStore::get().install( G_OBJECT_CLASS(cls), { { Property::ICON, "icon", "Icon", "Icon based on torrent's likely MIME type", &Torrent::get_icon }, { Property::NAME, "name", "Name", "Torrent name / title", &Torrent::get_name }, { Property::PERCENT_DONE, "percent-done", "Percent done", "Percent done (0..1) for current activity (leeching or seeding)", &Torrent::get_percent_done_fraction }, { Property::SHORT_STATUS, "short-status", "Short status", "Status text displayed in compact view mode", &Torrent::get_short_status_text }, { Property::LONG_PROGRESS, "long-progress", "Long progress", "Progress text displayed in full view mode", &Torrent::get_long_progress_text }, { Property::LONG_STATUS, "long-status", "Long status", "Status text displayed in full view mode", &Torrent::get_long_status_text }, { Property::SENSITIVE, "sensitive", "Sensitive", "Visual sensitivity of the view item, unrelated to activation possibility", &Torrent::get_sensitive }, { Property::CSS_CLASSES, "css-classes", "CSS classes", "CSS class names used for styling view items", &Torrent::get_css_classes }, }); } Glib::ustring Torrent::Impl::get_short_transfer_text() const { if (cache_.has_metadata && cache_.active_peers_down > 0) { return fmt::format( _("{download_speed} ▼ {upload_speed} ▲"), fmt::arg("upload_speed", tr_formatter_speed_KBps(cache_.speed_up)), fmt::arg("download_speed", tr_formatter_speed_KBps(cache_.speed_down))); } if (cache_.has_metadata && cache_.active_peers_up > 0) { return fmt::format(_("{upload_speed} ▲"), fmt::arg("upload_speed", tr_formatter_speed_KBps(cache_.speed_up))); } if (cache_.stalled) { return _("Stalled"); } return {}; } Glib::ustring Torrent::Impl::get_error_text() const { switch (cache_.error_code) { case TR_STAT_TRACKER_WARNING: return fmt::format(_("Tracker warning: '{warning}'"), fmt::arg("warning", cache_.error_message)); case TR_STAT_TRACKER_ERROR: return fmt::format(_("Tracker Error: '{error}'"), fmt::arg("error", cache_.error_message)); case TR_STAT_LOCAL_ERROR: return fmt::format(_("Local error: '{error}'"), fmt::arg("error", cache_.error_message)); default: return {}; } } Glib::ustring Torrent::Impl::get_activity_text() const { switch (cache_.activity) { case TR_STATUS_STOPPED: case TR_STATUS_CHECK_WAIT: case TR_STATUS_CHECK: case TR_STATUS_DOWNLOAD_WAIT: case TR_STATUS_SEED_WAIT: return get_short_status_text(); case TR_STATUS_DOWNLOAD: if (!cache_.has_metadata) { return fmt::format( ngettext( // xgettext:no-c-format "Downloading metadata from {active_count} connected peer ({percent_done}% done)", "Downloading metadata from {active_count} connected peers ({percent_done}% done)", cache_.peers_connected), fmt::arg("active_count", cache_.peers_connected), fmt::arg("percent_done", cache_.metadata_percent_complete.to_string())); } if (cache_.peers_sending_to_us != 0 && cache_.webseeds_sending_to_us != 0) { return fmt::format( ngettext( "Downloading from {active_count} of {connected_count} connected peer and webseed", "Downloading from {active_count} of {connected_count} connected peers and webseeds", cache_.peers_connected + cache_.webseeds_sending_to_us), fmt::arg("active_count", cache_.peers_sending_to_us + cache_.webseeds_sending_to_us), fmt::arg("connected_count", cache_.peers_connected + cache_.webseeds_sending_to_us)); } if (cache_.webseeds_sending_to_us != 0) { return fmt::format( ngettext( "Downloading from {active_count} webseed", "Downloading from {active_count} webseeds", cache_.webseeds_sending_to_us), fmt::arg("active_count", cache_.webseeds_sending_to_us)); } return fmt::format( ngettext( "Downloading from {active_count} of {connected_count} connected peer", "Downloading from {active_count} of {connected_count} connected peers", cache_.peers_connected), fmt::arg("active_count", cache_.peers_sending_to_us), fmt::arg("connected_count", cache_.peers_connected)); case TR_STATUS_SEED: return fmt::format( ngettext( "Seeding to {active_count} of {connected_count} connected peer", "Seeding to {active_count} of {connected_count} connected peers", cache_.peers_connected), fmt::arg("active_count", cache_.peers_getting_from_us), fmt::arg("connected_count", cache_.peers_connected)); default: g_assert_not_reached(); return {}; } } Torrent::Torrent() : Glib::ObjectBase(typeid(Torrent)) , ExtraClassInit(&Impl::class_init) { } Torrent::Torrent(tr_torrent* torrent) : Glib::ObjectBase(typeid(Torrent)) , ExtraClassInit(&Impl::class_init) , impl_(std::make_unique(*this, torrent)) { g_assert(torrent != nullptr); } Glib::ustring const& Torrent::get_name_collated() const { return impl_->get_cache().name_collated; } tr_torrent_id_t Torrent::get_id() const { return tr_torrentId(impl_->get_raw_torrent()); } tr_torrent& Torrent::get_underlying() const { return *impl_->get_raw_torrent(); } float Torrent::get_speed_up() const { return impl_->get_cache().speed_up; } float Torrent::get_speed_down() const { return impl_->get_cache().speed_down; } int Torrent::get_active_peers_up() const { return impl_->get_cache().active_peers_up; } int Torrent::get_active_peers_down() const { return impl_->get_cache().active_peers_down; } Percents Torrent::get_recheck_progress() const { return impl_->get_cache().recheck_progress; } bool Torrent::get_active() const { return impl_->get_cache().active; } tr_torrent_activity Torrent::get_activity() const { return impl_->get_cache().activity; } bool Torrent::get_finished() const { return impl_->get_cache().finished; } tr_priority_t Torrent::get_priority() const { return impl_->get_cache().priority; } size_t Torrent::get_queue_position() const { return impl_->get_cache().queue_position; } unsigned int Torrent::get_trackers() const { return impl_->get_cache().trackers; } int Torrent::get_error_code() const { return impl_->get_cache().error_code; } Glib::ustring const& Torrent::get_error_message() const { return impl_->get_cache().error_message; } int Torrent::get_active_peer_count() const { return impl_->get_cache().active_peer_count; } uint64_t Torrent::get_total_size() const { return impl_->get_cache().total_size; } float Torrent::get_ratio() const { return impl_->get_cache().ratio; } time_t Torrent::get_eta() const { return impl_->get_cache().eta; } time_t Torrent::get_added_date() const { return impl_->get_cache().added_date; } Percents Torrent::get_percent_complete() const { return impl_->get_cache().percent_complete; } Percents Torrent::get_seed_ratio_percent_done() const { return impl_->get_cache().seed_ratio_percent_done; } Glib::RefPtr Torrent::get_icon() const { return impl_->get_icon(); } Glib::ustring Torrent::get_name() const { return impl_->get_cache().name; } Percents Torrent::get_percent_done() const { return impl_->get_cache().activity_percent_done; } float Torrent::get_percent_done_fraction() const { return get_percent_done().to_fraction(); } Glib::ustring Torrent::get_short_status_text() const { return impl_->get_short_status_text(); } Glib::ustring Torrent::get_long_progress_text() const { return impl_->get_long_progress_text(); } Glib::ustring Torrent::get_long_status_text() const { return impl_->get_long_status_text(); } bool Torrent::get_sensitive() const { return impl_->get_cache().activity != TR_STATUS_STOPPED; } std::vector Torrent::get_css_classes() const { return impl_->get_css_classes(); } Torrent::ChangeFlags Torrent::update() { auto result = impl_->update_cache(); impl_->notify_property_changes(result); return result; } Glib::RefPtr Torrent::create(tr_torrent* torrent) { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) return Glib::make_refptr_for_instance(new Torrent(torrent)); } Torrent::Columns const& Torrent::get_columns() { static Columns const columns; return columns; } int Torrent::get_item_id(Glib::RefPtr const& item) { if (auto const torrent = gtr_ptr_dynamic_cast(item); torrent != nullptr) { return torrent->get_id(); } return 0; } void Torrent::get_item_value(Glib::RefPtr const& item, int column, Glib::ValueBase& value) { if (auto const torrent = gtr_ptr_dynamic_cast(item); torrent != nullptr) { torrent->impl_->get_value(column, value); } } int Torrent::compare_by_id(Glib::RefPtr const& lhs, Glib::RefPtr const& rhs) { return tr_compare_3way(lhs->get_id(), rhs->get_id()); } bool Torrent::less_by_id(Glib::RefPtr const& lhs, Glib::RefPtr const& rhs) { return lhs->get_id() < rhs->get_id(); }