mirror of
https://github.com/transmission/transmission
synced 2025-01-30 10:52:00 +00:00
feat: add generic favicon_cache class for reuse between Qt, GTK app (#5294)
This commit is contained in:
parent
9d2507c7e3
commit
1b6b85989c
21 changed files with 385 additions and 441 deletions
|
@ -13,7 +13,6 @@ target_sources(${TR_NAME}-gtk
|
|||
Dialogs.cc
|
||||
Dialogs.h
|
||||
FaviconCache.cc
|
||||
FaviconCache.h
|
||||
FileList.cc
|
||||
FileList.h
|
||||
FilterBar.cc
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
#include "DetailsDialog.h"
|
||||
|
||||
#include "Actions.h"
|
||||
#include "FaviconCache.h" // gtr_get_favicon()
|
||||
#include "FileList.h"
|
||||
#include "GtkCompat.h"
|
||||
#include "HigWorkarea.h" // GUI_PAD, GUI_PAD_BIG, GUI_PAD_SMALL
|
||||
|
@ -2046,16 +2045,16 @@ tr_torrent* DetailsDialog::Impl::tracker_list_get_current_torrent() const
|
|||
namespace
|
||||
{
|
||||
|
||||
void favicon_ready_cb(Glib::RefPtr<Gdk::Pixbuf> const& pixbuf, Gtk::TreeRowReference& reference)
|
||||
void favicon_ready_cb(Glib::RefPtr<Gdk::Pixbuf> const* pixbuf, Gtk::TreeRowReference& reference)
|
||||
{
|
||||
if (pixbuf != nullptr)
|
||||
if (pixbuf != nullptr && *pixbuf != nullptr)
|
||||
{
|
||||
auto const path = reference.get_path();
|
||||
auto const model = reference.get_model();
|
||||
|
||||
if (auto const iter = model->get_iter(path); iter)
|
||||
{
|
||||
(*iter)[tracker_cols.favicon] = pixbuf;
|
||||
(*iter)[tracker_cols.favicon] = *pixbuf;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2067,7 +2066,6 @@ void DetailsDialog::Impl::refreshTracker(std::vector<tr_torrent*> const& torrent
|
|||
std::ostringstream gstr;
|
||||
auto& hash = tracker_hash_;
|
||||
auto const& store = tracker_store_;
|
||||
auto* session = core_->get_session();
|
||||
bool const showScrape = scrape_check_->get_active();
|
||||
|
||||
/* step 1: get all the trackers */
|
||||
|
@ -2104,10 +2102,10 @@ void DetailsDialog::Impl::refreshTracker(std::vector<tr_torrent*> const& torrent
|
|||
|
||||
auto const p = store->get_path(iter);
|
||||
hash.try_emplace(gstr.str(), Gtk::TreeRowReference(store, p));
|
||||
gtr_get_favicon_from_url(
|
||||
session,
|
||||
core_->favicon_cache().load(
|
||||
tracker.announce,
|
||||
[ref = Gtk::TreeRowReference(store, p)](auto const& pixbuf) mutable { favicon_ready_cb(pixbuf, ref); });
|
||||
[ref = Gtk::TreeRowReference(store, p)](auto const* pixbuf_refptr) mutable
|
||||
{ favicon_ready_cb(pixbuf_refptr, ref); });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,164 +3,52 @@
|
|||
// or any future license endorsed by Mnemosyne LLC.
|
||||
// License text can be found in the licenses/ folder.
|
||||
|
||||
#include "FaviconCache.h"
|
||||
|
||||
#include "Utils.h" /* gtr_get_host_from_url() */
|
||||
|
||||
#include <libtransmission/transmission.h>
|
||||
#include <libtransmission/web-utils.h>
|
||||
#include <libtransmission/web.h> // tr_sessionFetch()
|
||||
#include <libtransmission/favicon-cache.h>
|
||||
|
||||
#include <gdkmm/pixbuf.h>
|
||||
#include <giomm/memoryinputstream.h>
|
||||
#include <glibmm/error.h>
|
||||
#include <glibmm/fileutils.h>
|
||||
#include <glibmm/main.h>
|
||||
#include <glibmm/miscutils.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
using Icon = Glib::RefPtr<Gdk::Pixbuf>;
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <glib/gstdio.h> /* g_remove() */
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace
|
||||
template<>
|
||||
Icon FaviconCache<Icon>::create_from_file(std::string_view filename) const
|
||||
{
|
||||
|
||||
constexpr auto TimeoutSecs = 15s;
|
||||
constexpr auto ImageTypes = std::array<std::string_view, 4>{ "ico"sv, "png"sv, "gif"sv, "jpg"sv };
|
||||
|
||||
struct favicon_data
|
||||
{
|
||||
tr_session* session = nullptr;
|
||||
std::function<void(Glib::RefPtr<Gdk::Pixbuf> const&)> func;
|
||||
std::string host;
|
||||
std::string contents;
|
||||
size_t type = 0;
|
||||
long code = 0;
|
||||
};
|
||||
|
||||
Glib::ustring get_url(std::string const& host, size_t image_type)
|
||||
{
|
||||
return fmt::format("http://{}/favicon.{}", host, ImageTypes.at(image_type));
|
||||
}
|
||||
|
||||
std::string favicon_get_cache_dir()
|
||||
{
|
||||
static std::string dir;
|
||||
|
||||
if (dir.empty())
|
||||
{
|
||||
dir = Glib::build_filename(Glib::get_user_cache_dir(), "transmission", "favicons");
|
||||
(void)g_mkdir_with_parents(dir.c_str(), 0777);
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
std::string favicon_get_cache_filename(std::string const& host)
|
||||
{
|
||||
return Glib::build_filename(favicon_get_cache_dir(), host);
|
||||
}
|
||||
|
||||
void favicon_save_to_cache(std::string const& host, std::string const& data)
|
||||
{
|
||||
Glib::file_set_contents(favicon_get_cache_filename(host), data);
|
||||
}
|
||||
|
||||
Glib::RefPtr<Gdk::Pixbuf> favicon_load_from_cache(std::string const& host)
|
||||
{
|
||||
auto const filename = favicon_get_cache_filename(host);
|
||||
|
||||
try
|
||||
{
|
||||
return Gdk::Pixbuf::create_from_file(filename, 16, 16, false);
|
||||
return Gdk::Pixbuf::create_from_file(std::string{ filename }, Width, Height, false);
|
||||
}
|
||||
catch (Glib::Error const&)
|
||||
{
|
||||
(void)g_remove(filename.c_str());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void favicon_web_done_cb(tr_web::FetchResponse const& response);
|
||||
|
||||
constexpr bool should_keep_trying(long code)
|
||||
template<>
|
||||
Icon FaviconCache<Icon>::create_from_data(void const* data, size_t datalen) const
|
||||
{
|
||||
return code != 0 && code <= 500;
|
||||
}
|
||||
|
||||
bool favicon_web_done_idle_cb(std::unique_ptr<favicon_data> fav)
|
||||
{
|
||||
Glib::RefPtr<Gdk::Pixbuf> pixbuf;
|
||||
|
||||
if (!fav->contents.empty()) /* we got something... try to make a pixbuf from it */
|
||||
try
|
||||
{
|
||||
favicon_save_to_cache(fav->host, fav->contents);
|
||||
pixbuf = favicon_load_from_cache(fav->host);
|
||||
auto memory_stream = Gio::MemoryInputStream::create();
|
||||
memory_stream->add_data(data, datalen, nullptr);
|
||||
return Gdk::Pixbuf::create_from_stream_at_scale(memory_stream, Width, Height, false);
|
||||
}
|
||||
|
||||
if (pixbuf == nullptr && should_keep_trying(fav->code) && ++fav->type < ImageTypes.size()) /* keep trying */
|
||||
catch (Glib::Error const&)
|
||||
{
|
||||
fav->contents.clear();
|
||||
auto* const session = fav->session;
|
||||
auto const next_url = get_url(fav->host, fav->type);
|
||||
tr_sessionFetch(session, { next_url.raw(), favicon_web_done_cb, fav.release(), TimeoutSecs });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Not released into the next web request, means we're done trying (even if `pixbuf` is still invalid)
|
||||
fav->func(pixbuf);
|
||||
return false;
|
||||
}
|
||||
|
||||
void favicon_web_done_cb(tr_web::FetchResponse const& response)
|
||||
{
|
||||
auto* const fav = static_cast<favicon_data*>(response.user_data);
|
||||
fav->contents = response.body;
|
||||
fav->code = response.status;
|
||||
Glib::signal_idle().connect([fav]() { return favicon_web_done_idle_cb(std::unique_ptr<favicon_data>(fav)); });
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void gtr_get_favicon(
|
||||
tr_session* session,
|
||||
std::string const& host,
|
||||
std::function<void(Glib::RefPtr<Gdk::Pixbuf> const&)> const& pixbuf_ready_func)
|
||||
{
|
||||
auto pixbuf = favicon_load_from_cache(host);
|
||||
|
||||
if (pixbuf != nullptr)
|
||||
{
|
||||
pixbuf_ready_func(pixbuf);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto data = std::make_unique<favicon_data>();
|
||||
data->session = session;
|
||||
data->func = pixbuf_ready_func;
|
||||
data->host = host;
|
||||
tr_sessionFetch(session, { get_url(host, 0).raw(), favicon_web_done_cb, data.release(), TimeoutSecs });
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void gtr_get_favicon_from_url(
|
||||
tr_session* session,
|
||||
Glib::ustring const& url,
|
||||
std::function<void(Glib::RefPtr<Gdk::Pixbuf> const&)> const& pixbuf_ready_func)
|
||||
template<>
|
||||
std::string FaviconCache<Icon>::app_cache_dir() const
|
||||
{
|
||||
if (auto const parsed_url = tr_urlParse(url.c_str()); parsed_url.has_value())
|
||||
{
|
||||
auto const host = std::string{ parsed_url->host };
|
||||
gtr_get_favicon(session, host, pixbuf_ready_func);
|
||||
}
|
||||
else
|
||||
{
|
||||
pixbuf_ready_func({});
|
||||
}
|
||||
return fmt::format("{:s}/{:s}", Glib::get_user_cache_dir(), "transmission");
|
||||
}
|
||||
|
||||
template<>
|
||||
void FaviconCache<Icon>::add_to_ui_thread(std::function<void()> idlefunc)
|
||||
{
|
||||
Glib::signal_idle().connect_once([idlefunc = std::move(idlefunc)]() { idlefunc(); });
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
// This file Copyright © 2012-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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <libtransmission/transmission.h>
|
||||
|
||||
#include <gdkmm/pixbuf.h>
|
||||
#include <glibmm/refptr.h>
|
||||
#include <glibmm/ustring.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
void gtr_get_favicon(
|
||||
tr_session* session,
|
||||
std::string const& host,
|
||||
std::function<void(Glib::RefPtr<Gdk::Pixbuf> const&)> const& pixbuf_ready_func);
|
||||
|
||||
void gtr_get_favicon_from_url(
|
||||
tr_session* session,
|
||||
Glib::ustring const& url,
|
||||
std::function<void(Glib::RefPtr<Gdk::Pixbuf> const&)> const& pixbuf_ready_func);
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
#include "FilterBar.h"
|
||||
|
||||
#include "FaviconCache.h" // gtr_get_favicon()
|
||||
#include "FilterListModel.hh"
|
||||
#include "HigWorkarea.h" // GUI_PAD
|
||||
#include "ListModelAdapter.h"
|
||||
|
@ -79,7 +78,7 @@ private:
|
|||
bool activity_filter_model_update();
|
||||
|
||||
bool tracker_filter_model_update();
|
||||
void favicon_ready_cb(Glib::RefPtr<Gdk::Pixbuf> const& pixbuf, Gtk::TreeModel::Path const& path);
|
||||
void favicon_ready_cb(Glib::RefPtr<Gdk::Pixbuf> const* pixbuf, Gtk::TreeModel::Path const& path);
|
||||
|
||||
void update_filter_models(Torrent::ChangeFlags changes);
|
||||
void update_filter_models_idle(Torrent::ChangeFlags changes);
|
||||
|
@ -172,13 +171,13 @@ void FilterBar::Impl::tracker_model_update_count(Gtk::TreeModel::iterator const&
|
|||
}
|
||||
}
|
||||
|
||||
void FilterBar::Impl::favicon_ready_cb(Glib::RefPtr<Gdk::Pixbuf> const& pixbuf, Gtk::TreeModel::Path const& path)
|
||||
void FilterBar::Impl::favicon_ready_cb(Glib::RefPtr<Gdk::Pixbuf> const* pixbuf, Gtk::TreeModel::Path const& path)
|
||||
{
|
||||
if (pixbuf != nullptr)
|
||||
if (pixbuf != nullptr && *pixbuf != nullptr)
|
||||
{
|
||||
if (auto const iter = tracker_model_->get_iter(path); iter)
|
||||
{
|
||||
iter->set_value(tracker_filter_cols.pixbuf, pixbuf);
|
||||
iter->set_value(tracker_filter_cols.pixbuf, *pixbuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -190,6 +189,7 @@ bool FilterBar::Impl::tracker_filter_model_update()
|
|||
int count = 0;
|
||||
std::string host;
|
||||
std::string sitename;
|
||||
std::string announce_url;
|
||||
|
||||
bool operator<(site_info const& that) const
|
||||
{
|
||||
|
@ -214,18 +214,19 @@ bool FilterBar::Impl::tracker_filter_model_update()
|
|||
|
||||
auto const& raw_torrent = torrent->get_underlying();
|
||||
|
||||
auto torrent_sites_and_hosts = std::map<std::string, std::string>{};
|
||||
auto site_to_host_and_announce = std::map<std::string, std::pair<std::string, std::string>>{};
|
||||
for (size_t j = 0, n = tr_torrentTrackerCount(&raw_torrent); j < n; ++j)
|
||||
{
|
||||
auto const view = tr_torrentTracker(&raw_torrent, j);
|
||||
torrent_sites_and_hosts.try_emplace(std::data(view.sitename), view.host);
|
||||
site_to_host_and_announce.try_emplace(std::data(view.sitename), view.host, view.announce);
|
||||
}
|
||||
|
||||
for (auto const& [sitename, host] : torrent_sites_and_hosts)
|
||||
for (auto const& [sitename, host_and_announce] : site_to_host_and_announce)
|
||||
{
|
||||
auto& info = site_infos[sitename];
|
||||
info.host = host_and_announce.first;
|
||||
info.announce_url = host_and_announce.second;
|
||||
info.sitename = sitename;
|
||||
info.host = host;
|
||||
++info.count;
|
||||
}
|
||||
|
||||
|
@ -299,10 +300,9 @@ bool FilterBar::Impl::tracker_filter_model_update()
|
|||
add->set_value(tracker_filter_cols.count, site.count);
|
||||
add->set_value(tracker_filter_cols.type, static_cast<int>(TrackerType::HOST));
|
||||
auto path = tracker_model_->get_path(add);
|
||||
gtr_get_favicon(
|
||||
core_->get_session(),
|
||||
site.host,
|
||||
[this, path](auto const& pixbuf) { favicon_ready_cb(pixbuf, path); });
|
||||
core_->favicon_cache().load(
|
||||
site.announce_url,
|
||||
[this, path](auto const* pixbuf) { favicon_ready_cb(pixbuf, path); });
|
||||
++i;
|
||||
}
|
||||
else // update row
|
||||
|
|
|
@ -147,6 +147,11 @@ public:
|
|||
return signal_torrents_changed_;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto& favicon_cache()
|
||||
{
|
||||
return favicon_cache_;
|
||||
}
|
||||
|
||||
private:
|
||||
Glib::RefPtr<Session> get_core_ptr() const;
|
||||
|
||||
|
@ -209,6 +214,8 @@ private:
|
|||
Glib::RefPtr<SortListModel<Torrent>> sorted_model_;
|
||||
Glib::RefPtr<TorrentSorter> sorter_ = TorrentSorter::create();
|
||||
tr_session* session_ = nullptr;
|
||||
|
||||
FaviconCache<Glib::RefPtr<Gdk::Pixbuf>> favicon_cache_;
|
||||
};
|
||||
|
||||
Glib::RefPtr<Session> Session::Impl::get_core_ptr() const
|
||||
|
@ -531,8 +538,8 @@ Session::Session(tr_session* session)
|
|||
Session::~Session() = default;
|
||||
|
||||
Session::Impl::Impl(Session& core, tr_session* session)
|
||||
: core_(core)
|
||||
, session_(session)
|
||||
: core_{ core }
|
||||
, session_{ session }
|
||||
{
|
||||
raw_model_ = Gio::ListStore<Torrent>::create();
|
||||
signal_torrents_changed_.connect(sigc::hide<0>(sigc::mem_fun(*sorter_.get(), &TorrentSorter::update)));
|
||||
|
@ -1359,6 +1366,11 @@ tr_torrent* Session::find_torrent(tr_torrent_id_t id) const
|
|||
return tor;
|
||||
}
|
||||
|
||||
FaviconCache<Glib::RefPtr<Gdk::Pixbuf>>& Session::favicon_cache() const
|
||||
{
|
||||
return impl_->favicon_cache();
|
||||
}
|
||||
|
||||
void Session::open_folder(tr_torrent_id_t torrent_id) const
|
||||
{
|
||||
auto const* tor = find_torrent(torrent_id);
|
||||
|
|
|
@ -8,9 +8,11 @@
|
|||
#include "Torrent.h"
|
||||
|
||||
#include <libtransmission/transmission.h>
|
||||
#include <libtransmission/favicon-cache.h>
|
||||
#include <libtransmission/tr-macros.h>
|
||||
#include <libtransmission/variant.h>
|
||||
|
||||
#include <gdkmm/pixbuf.h>
|
||||
#include <giomm/file.h>
|
||||
#include <giomm/listmodel.h>
|
||||
#include <glibmm/object.h>
|
||||
|
@ -58,6 +60,8 @@ public:
|
|||
|
||||
tr_torrent* find_torrent(tr_torrent_id_t id) const;
|
||||
|
||||
FaviconCache<Glib::RefPtr<Gdk::Pixbuf>>& favicon_cache() const;
|
||||
|
||||
/******
|
||||
*******
|
||||
******/
|
||||
|
|
|
@ -50,6 +50,7 @@ target_sources(${TR_NAME}
|
|||
error-types.h
|
||||
error.cc
|
||||
error.h
|
||||
favicon-cache.h
|
||||
file-piece-map.cc
|
||||
file-piece-map.h
|
||||
file-posix.cc
|
||||
|
@ -304,6 +305,7 @@ if(INSTALL_LIB)
|
|||
FILES
|
||||
error-types.h
|
||||
error.h
|
||||
favicon-cache.h
|
||||
file.h
|
||||
log.h
|
||||
makemeta.h
|
||||
|
|
245
libtransmission/favicon-cache.h
Normal file
245
libtransmission/favicon-cache.h
Normal file
|
@ -0,0 +1,245 @@
|
|||
// This file Copyright © 2012-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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <libtransmission/file.h>
|
||||
#include <libtransmission/utils.h>
|
||||
#include <libtransmission/web-utils.h>
|
||||
#include <libtransmission/web.h>
|
||||
|
||||
template<typename Icon>
|
||||
class FaviconCache
|
||||
{
|
||||
public:
|
||||
using IconFunc = std::function<void(Icon const*)>;
|
||||
|
||||
FaviconCache()
|
||||
: cache_dir_{ app_cache_dir() }
|
||||
, icons_dir_{ fmt::format("{:s}/{:s}", cache_dir_, "favicons") }
|
||||
, scraped_sitenames_filename_{ fmt::format("{:s}/favicons-scraped.txt", cache_dir_) }
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] Icon const* find(std::string_view sitename) const noexcept
|
||||
{
|
||||
auto const iter = icons_.find(sitename);
|
||||
return iter != std::end(icons_) ? &iter->second : nullptr;
|
||||
}
|
||||
|
||||
void load(
|
||||
std::string_view url_in,
|
||||
IconFunc callback = [](Icon const&) {})
|
||||
{
|
||||
std::call_once(scan_once_flag_, &FaviconCache::scan_file_cache, this);
|
||||
|
||||
auto const url = tr_urlParse(url_in);
|
||||
if (!url) // invalid url?
|
||||
{
|
||||
callback({});
|
||||
return;
|
||||
}
|
||||
|
||||
// If we already have it, use it.
|
||||
if (auto const* const icon = find(url->sitename); icon != nullptr)
|
||||
{
|
||||
callback(icon);
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't already have it, so fetch it.
|
||||
// Add a placeholder to icons_ prevent repeat downloads.
|
||||
icons_.try_emplace(std::string{ url->sitename });
|
||||
mark_site_as_scraped(url->sitename);
|
||||
|
||||
// ports to try
|
||||
auto n_ports = 0;
|
||||
auto ports = std::array<int, 2>{};
|
||||
ports[n_ports++] = 80;
|
||||
if (url->port != 80)
|
||||
{
|
||||
ports[n_ports++] = url->port;
|
||||
}
|
||||
|
||||
auto in_flight = std::make_shared<InFlightData>(callback, url->sitename);
|
||||
for (auto i = 0; i < n_ports; ++i)
|
||||
{
|
||||
for (auto const scheme : { "http", "https" })
|
||||
{
|
||||
for (auto const suffix : { "ico", "png", "gif", "jpg" })
|
||||
{
|
||||
auto on_fetch_response = [this, in_flight](auto const& response)
|
||||
{
|
||||
in_flight->add_response(response.body, response.status);
|
||||
add_to_ui_thread([this, in_flight]() { check_responses(in_flight); });
|
||||
};
|
||||
|
||||
static constexpr auto TimeoutSecs = std::chrono::seconds{ 15 };
|
||||
auto const favicon_url = fmt::format("{:s}://{:s}:{:d}/favicon.{:s}", scheme, url->host, ports[i], suffix);
|
||||
in_flight->web().fetch({ favicon_url, std::move(on_fetch_response), nullptr, TimeoutSecs });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static inline constexpr auto Width = 16;
|
||||
static inline constexpr auto Height = 16;
|
||||
|
||||
private:
|
||||
class InFlightData
|
||||
{
|
||||
public:
|
||||
InFlightData(IconFunc callback, std::string_view sitename)
|
||||
: callback_{ std::move(callback) }
|
||||
, sitename_{ sitename }
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto const& sitename() const noexcept
|
||||
{
|
||||
return sitename_;
|
||||
}
|
||||
|
||||
~InFlightData()
|
||||
{
|
||||
invoke_callback(nullptr); // ensure it's called once, even if no icon
|
||||
}
|
||||
|
||||
void invoke_callback(Icon const* icon)
|
||||
{
|
||||
if (callback_)
|
||||
{
|
||||
callback_(icon);
|
||||
callback_ = {};
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] auto get_responses()
|
||||
{
|
||||
auto lock = std::lock_guard{ responses_mutex_ };
|
||||
|
||||
auto tmp = decltype(responses_){};
|
||||
std::swap(tmp, responses_);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
void add_response(std::string contents, long code)
|
||||
{
|
||||
auto lock = std::lock_guard{ responses_mutex_ };
|
||||
|
||||
responses_.emplace_back(std::move(contents), code);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto& web()
|
||||
{
|
||||
return *web_;
|
||||
}
|
||||
|
||||
private:
|
||||
IconFunc callback_;
|
||||
std::string const sitename_;
|
||||
|
||||
std::mutex responses_mutex_;
|
||||
std::vector<std::pair<std::string, long>> responses_;
|
||||
|
||||
tr_web::Mediator mediator_;
|
||||
std::unique_ptr<tr_web> web_ = tr_web::create(mediator_);
|
||||
};
|
||||
|
||||
[[nodiscard]] Icon create_from_file(std::string_view filename) const;
|
||||
[[nodiscard]] Icon create_from_data(void const* data, size_t datalen) const;
|
||||
[[nodiscard]] std::string app_cache_dir() const;
|
||||
void add_to_ui_thread(std::function<void()> idlefunc);
|
||||
|
||||
void scan_file_cache()
|
||||
{
|
||||
// ensure the folders exist
|
||||
tr_sys_dir_create(cache_dir_, TR_SYS_DIR_CREATE_PARENTS, 0700);
|
||||
tr_sys_dir_create(icons_dir_, TR_SYS_DIR_CREATE_PARENTS, 0700);
|
||||
|
||||
// remember which hosts we've asked for a favicon so that we
|
||||
// don't re-ask them every time we start a new session
|
||||
if (auto ifs = std::ifstream{ scraped_sitenames_filename_ }; ifs.is_open())
|
||||
{
|
||||
auto sitename = std::string{};
|
||||
while (std::getline(ifs, sitename))
|
||||
{
|
||||
icons_.try_emplace(sitename);
|
||||
}
|
||||
}
|
||||
|
||||
// load the cached favicons
|
||||
for (auto const& sitename : tr_sys_dir_get_files(icons_dir_))
|
||||
{
|
||||
auto const filename = fmt::format("{:s}/{:s}", icons_dir_, sitename);
|
||||
|
||||
if (auto icon = create_from_file(filename); !icon)
|
||||
{
|
||||
tr_sys_path_remove(filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
icons_[sitename] = std::move(icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mark_site_as_scraped(std::string_view sitename)
|
||||
{
|
||||
if (auto ofs = std::ofstream{ scraped_sitenames_filename_, std::ios_base::out | std::ios_base::app }; ofs.is_open())
|
||||
{
|
||||
ofs << sitename << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void check_responses(std::shared_ptr<FaviconCache::InFlightData> in_flight)
|
||||
{
|
||||
for (auto const& [contents, code] : in_flight->get_responses())
|
||||
{
|
||||
if (std::empty(contents) || code < 200 || code >= 300)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto const icon = create_from_data(std::data(contents), std::size(contents));
|
||||
if (!icon)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// cache it in memory
|
||||
auto& perm = icons_[in_flight->sitename()];
|
||||
perm = std::move(icon);
|
||||
|
||||
// cache it on disk
|
||||
tr_saveFile(fmt::format("{:s}/{:s}", icons_dir_, in_flight->sitename()), contents);
|
||||
|
||||
// notify the user that we got it
|
||||
in_flight->invoke_callback(&perm);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::once_flag scan_once_flag_;
|
||||
std::string const cache_dir_;
|
||||
std::string const icons_dir_;
|
||||
std::string const scraped_sitenames_filename_;
|
||||
|
||||
std::map<std::string /*sitename*/, Icon, std::less<>> icons_;
|
||||
};
|
|
@ -156,7 +156,10 @@ public:
|
|||
func(response);
|
||||
}
|
||||
|
||||
[[nodiscard]] virtual time_t now() const = 0;
|
||||
[[nodiscard]] virtual time_t now() const
|
||||
{
|
||||
return time(nullptr);
|
||||
}
|
||||
};
|
||||
|
||||
// Note that tr_web does no management of the `mediator` reference.
|
||||
|
|
|
@ -656,11 +656,6 @@ void Application::onNotificationActionInvoked(quint32 /* notification_id */, QSt
|
|||
}
|
||||
#endif
|
||||
|
||||
FaviconCache& Application::faviconCache()
|
||||
{
|
||||
return favicons_;
|
||||
}
|
||||
|
||||
/***
|
||||
****
|
||||
***/
|
||||
|
|
|
@ -13,10 +13,11 @@
|
|||
#include <QRegularExpression>
|
||||
#include <QTimer>
|
||||
#include <QTranslator>
|
||||
#include <QWeakPointer>
|
||||
|
||||
#include <libtransmission/tr-macros.h>
|
||||
#include <libtransmission/favicon-cache.h>
|
||||
|
||||
#include "FaviconCache.h"
|
||||
#include "Typedefs.h"
|
||||
#include "Utils.h" // std::hash<QString>
|
||||
|
||||
|
@ -44,7 +45,30 @@ public:
|
|||
return *interned_strings_.insert(in).first;
|
||||
}
|
||||
|
||||
FaviconCache& faviconCache();
|
||||
[[nodiscard]] QPixmap find_favicon(QString const& sitename) const
|
||||
{
|
||||
auto const key = sitename.toStdString();
|
||||
auto const* const icon = favicon_cache_.find(key);
|
||||
return icon != nullptr ? *icon : QPixmap{};
|
||||
}
|
||||
|
||||
void load_favicon(QString const& url)
|
||||
{
|
||||
auto weak_self = QPointer<Application>{ this };
|
||||
|
||||
favicon_cache_.load(
|
||||
url.toStdString(),
|
||||
[weak_self = std::move(weak_self)](QPixmap const* /*favicon_or_nullptr*/)
|
||||
{
|
||||
if (!weak_self.isNull())
|
||||
{
|
||||
weak_self.data()->faviconsChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
signals:
|
||||
void faviconsChanged();
|
||||
|
||||
public slots:
|
||||
void addTorrent(AddData const&) const;
|
||||
|
@ -82,7 +106,8 @@ private:
|
|||
time_t last_full_update_time_ = {};
|
||||
QTranslator qt_translator_;
|
||||
QTranslator app_translator_;
|
||||
FaviconCache favicons_;
|
||||
|
||||
FaviconCache<QPixmap> favicon_cache_;
|
||||
|
||||
QString const config_name_ = QStringLiteral("transmission");
|
||||
QString const display_name_ = QStringLiteral("transmission-qt");
|
||||
|
|
|
@ -21,7 +21,6 @@ target_sources(${TR_NAME}-qt
|
|||
DetailsDialog.cc
|
||||
DetailsDialog.h
|
||||
FaviconCache.cc
|
||||
FaviconCache.h
|
||||
FileTreeDelegate.cc
|
||||
FileTreeDelegate.h
|
||||
FileTreeItem.cc
|
||||
|
|
|
@ -1,211 +1,49 @@
|
|||
// This file Copyright © 2012-2023 Mnemosyne LLC.
|
||||
// This file Copyright © 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 <array>
|
||||
#include <libtransmission/favicon-cache.h>
|
||||
|
||||
#include <QDir>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QApplication>
|
||||
#include <QPixmap>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include "FaviconCache.h"
|
||||
using Icon = QPixmap;
|
||||
|
||||
/***
|
||||
****
|
||||
***/
|
||||
|
||||
FaviconCache::FaviconCache()
|
||||
: nam_(new QNetworkAccessManager(this))
|
||||
template<>
|
||||
Icon FaviconCache<Icon>::create_from_file(std::string_view filename) const // NOLINT(readability-identifier-naming)
|
||||
{
|
||||
connect(nam_, &QNetworkAccessManager::finished, this, &FaviconCache::onRequestFinished);
|
||||
}
|
||||
|
||||
/***
|
||||
****
|
||||
***/
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
QPixmap scale(QPixmap const& pixmap)
|
||||
{
|
||||
return pixmap.scaled(FaviconCache::getIconSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
QString getCacheDir()
|
||||
{
|
||||
auto const base = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||
return QDir(base).absoluteFilePath(QStringLiteral("favicons"));
|
||||
}
|
||||
|
||||
QString getScrapedFile()
|
||||
{
|
||||
auto const base = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||
return QDir(base).absoluteFilePath(QStringLiteral("favicons-scraped.txt"));
|
||||
}
|
||||
|
||||
void markSiteAsScraped(QString const& sitename)
|
||||
{
|
||||
auto skip_file = QFile(getScrapedFile());
|
||||
if (skip_file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append))
|
||||
auto icon = QPixmap{};
|
||||
if (!icon.load(QString::fromUtf8(std::data(filename), std::size(filename))))
|
||||
{
|
||||
skip_file.write(sitename.toUtf8());
|
||||
skip_file.write("\n");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void FaviconCache::ensureCacheDirHasBeenScanned()
|
||||
{
|
||||
static bool has_been_scanned = false;
|
||||
if (has_been_scanned)
|
||||
{
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
|
||||
has_been_scanned = true;
|
||||
|
||||
// remember which hosts we've asked for a favicon so that we
|
||||
// don't re-ask them every time we start a new session
|
||||
if (auto skip_file = QFile(getScrapedFile()); skip_file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
while (!skip_file.atEnd())
|
||||
{
|
||||
auto const sitename = QString::fromUtf8(skip_file.readLine()).trimmed();
|
||||
pixmaps_.try_emplace(sitename);
|
||||
}
|
||||
}
|
||||
|
||||
// load the cached favicons
|
||||
auto cache_dir = QDir(getCacheDir());
|
||||
cache_dir.mkpath(cache_dir.absolutePath());
|
||||
for (auto const& sitename : cache_dir.entryList(QDir::Files | QDir::Readable))
|
||||
{
|
||||
QPixmap const pixmap(cache_dir.absoluteFilePath(sitename));
|
||||
if (!pixmap.isNull())
|
||||
{
|
||||
pixmaps_[sitename] = scale(pixmap);
|
||||
}
|
||||
}
|
||||
return icon.scaled({ Width, Height }, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
/***
|
||||
****
|
||||
***/
|
||||
|
||||
QString FaviconCache::getDisplayName(QString const& sitename)
|
||||
template<>
|
||||
Icon FaviconCache<Icon>::create_from_data(void const* data, size_t datalen) const // NOLINT(readability-identifier-naming)
|
||||
{
|
||||
auto name = sitename;
|
||||
if (!name.isEmpty())
|
||||
auto icon = QPixmap{};
|
||||
if (!icon.loadFromData(static_cast<uchar const*>(data), datalen))
|
||||
{
|
||||
name.front() = name.front().toTitleCase();
|
||||
return {};
|
||||
}
|
||||
return name;
|
||||
|
||||
return icon.scaled({ Width, Height }, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
QSize FaviconCache::getIconSize()
|
||||
template<>
|
||||
std::string FaviconCache<Icon>::app_cache_dir() const // NOLINT(readability-identifier-naming)
|
||||
{
|
||||
return { 16, 16 };
|
||||
return QStandardPaths::writableLocation(QStandardPaths::CacheLocation).toStdString();
|
||||
}
|
||||
|
||||
QPixmap FaviconCache::find(QString const& sitename)
|
||||
template<>
|
||||
void FaviconCache<Icon>::add_to_ui_thread(std::function<void()> idlefunc) // NOLINT(readability-identifier-naming)
|
||||
{
|
||||
ensureCacheDirHasBeenScanned();
|
||||
|
||||
return pixmaps_[sitename];
|
||||
}
|
||||
|
||||
void FaviconCache::add(QString const& sitename, QString const& url_str)
|
||||
{
|
||||
ensureCacheDirHasBeenScanned();
|
||||
|
||||
// Try to download a favicon if we don't have one.
|
||||
// Add a placeholder to prevent repeat downloads.
|
||||
if (auto const already_had_it = !pixmaps_.try_emplace(sitename).second; already_had_it)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
markSiteAsScraped(sitename);
|
||||
|
||||
auto const scrape = [this, sitename](auto const host)
|
||||
{
|
||||
auto const schemes = std::array<QString, 2>{
|
||||
QStringLiteral("http"),
|
||||
QStringLiteral("https"),
|
||||
};
|
||||
auto const suffixes = std::array<QString, 5>{
|
||||
QStringLiteral("gif"), //
|
||||
QStringLiteral("ico"), //
|
||||
QStringLiteral("jpg"), //
|
||||
QStringLiteral("png"), //
|
||||
QStringLiteral("svg"), //
|
||||
};
|
||||
for (auto const& scheme : schemes)
|
||||
{
|
||||
for (auto const& suffix : suffixes)
|
||||
{
|
||||
auto const path = QStringLiteral("%1://%2/favicon.%3").arg(scheme).arg(host).arg(suffix);
|
||||
auto request = QNetworkRequest(path);
|
||||
request.setAttribute(QNetworkRequest::UserMax, sitename);
|
||||
nam_->get(request);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// scrape tracker.domain.com
|
||||
auto const host = QUrl(url_str).host();
|
||||
scrape(host);
|
||||
|
||||
if (auto const idx = host.indexOf(sitename); idx != -1)
|
||||
{
|
||||
// scrape domain.com
|
||||
auto const root = host.mid(idx);
|
||||
if (root != host)
|
||||
{
|
||||
scrape(root);
|
||||
}
|
||||
|
||||
// scrape www.domain.com
|
||||
if (auto const www = QStringLiteral("www.") + root; www != host)
|
||||
{
|
||||
scrape(www);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FaviconCache::onRequestFinished(QNetworkReply* reply)
|
||||
{
|
||||
auto const content = reply->readAll();
|
||||
auto pixmap = QPixmap{};
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError)
|
||||
{
|
||||
pixmap.loadFromData(content);
|
||||
}
|
||||
|
||||
if (!pixmap.isNull())
|
||||
{
|
||||
auto sitename = reply->request().attribute(QNetworkRequest::UserMax).toString();
|
||||
|
||||
// save it in memory...
|
||||
pixmaps_[sitename] = scale(pixmap);
|
||||
|
||||
// save it on disk...
|
||||
QDir const cache_dir(getCacheDir());
|
||||
cache_dir.mkpath(cache_dir.absolutePath());
|
||||
QFile file(cache_dir.absoluteFilePath(sitename));
|
||||
file.open(QIODevice::WriteOnly);
|
||||
file.write(content);
|
||||
file.close();
|
||||
|
||||
// notify listeners
|
||||
emit pixmapReady(sitename);
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
QMetaObject::invokeMethod(qApp, std::move(idlefunc), Qt::QueuedConnection);
|
||||
}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
// This file Copyright © 2012-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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include <QObject>
|
||||
#include <QPixmap>
|
||||
#include <QString>
|
||||
|
||||
#include "Utils.h" // std::hash<QString>
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class QUrl;
|
||||
|
||||
class FaviconCache : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
FaviconCache();
|
||||
|
||||
// This will emit a signal when (if) the icon becomes ready.
|
||||
void add(QString const& sitename, QString const& url);
|
||||
|
||||
// returns a cached pixmap, or a nullptr pixmap if there's no match in the cache
|
||||
QPixmap find(QString const& sitename);
|
||||
|
||||
static QString getDisplayName(QString const& sitename);
|
||||
static QSize getIconSize();
|
||||
|
||||
signals:
|
||||
void pixmapReady(QString const& sitename);
|
||||
|
||||
private slots:
|
||||
void onRequestFinished(QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
void ensureCacheDirHasBeenScanned();
|
||||
|
||||
QNetworkAccessManager* nam_ = {};
|
||||
std::unordered_map<QString /*sitename*/, QPixmap> pixmaps_;
|
||||
};
|
|
@ -16,7 +16,6 @@
|
|||
#include <QStandardItemModel>
|
||||
|
||||
#include "Application.h"
|
||||
#include "FaviconCache.h"
|
||||
#include "FilterBarComboBox.h"
|
||||
#include "FilterBarComboBoxDelegate.h"
|
||||
#include "Filters.h"
|
||||
|
@ -93,7 +92,7 @@ FilterBarComboBox* FilterBar::createActivityCombo()
|
|||
namespace
|
||||
{
|
||||
|
||||
QString getCountString(size_t n)
|
||||
[[nodiscard]] auto getCountString(size_t n)
|
||||
{
|
||||
return QStringLiteral("%L1").arg(n);
|
||||
}
|
||||
|
@ -102,6 +101,18 @@ Torrent::fields_t constexpr TrackerFields = {
|
|||
static_cast<uint64_t>(1) << Torrent::TRACKER_STATS,
|
||||
};
|
||||
|
||||
[[nodiscard]] auto displayName(QString const& sitename)
|
||||
{
|
||||
auto name = sitename;
|
||||
|
||||
if (!name.isEmpty())
|
||||
{
|
||||
name.front() = name.front().toTitleCase();
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
auto constexpr ActivityFields = FilterMode::TorrentFields;
|
||||
|
||||
} // namespace
|
||||
|
@ -133,13 +144,12 @@ void FilterBar::refreshTrackers()
|
|||
auto update_tracker_item = [](QStandardItem* i, auto const& it)
|
||||
{
|
||||
auto const& [sitename, count] = *it;
|
||||
auto const display_name = FaviconCache::getDisplayName(sitename);
|
||||
auto const icon = trApp->faviconCache().find(sitename);
|
||||
auto const display_name = displayName(sitename);
|
||||
|
||||
i->setData(display_name, Qt::DisplayRole);
|
||||
i->setData(display_name, TRACKER_ROLE);
|
||||
i->setData(getCountString(static_cast<size_t>(count)), FilterBarComboBox::CountStringRole);
|
||||
i->setData(icon, Qt::DecorationRole);
|
||||
i->setData(trApp->find_favicon(sitename), Qt::DecorationRole);
|
||||
i->setData(static_cast<int>(count), FilterBarComboBox::CountRole);
|
||||
|
||||
return i;
|
||||
|
@ -243,7 +253,7 @@ FilterBar::FilterBar(Prefs& prefs, TorrentModel const& torrents, TorrentFilter c
|
|||
connect(&torrents_, &TorrentModel::rowsRemoved, this, &FilterBar::recountAllSoon);
|
||||
connect(&torrents_, &TorrentModel::torrentsChanged, this, &FilterBar::onTorrentsChanged);
|
||||
connect(&recount_timer_, &QTimer::timeout, this, &FilterBar::recount);
|
||||
connect(&trApp->faviconCache(), &FaviconCache::pixmapReady, this, &FilterBar::recountTrackersSoon);
|
||||
connect(trApp, &Application::faviconsChanged, this, &FilterBar::recountTrackersSoon);
|
||||
|
||||
recountAllSoon();
|
||||
is_bootstrapping_ = false; // NOLINT cppcoreguidelines-prefer-member-initializer
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
|
||||
#include <libtransmission/tr-macros.h>
|
||||
|
||||
#include "FaviconCache.h"
|
||||
#include "Torrent.h"
|
||||
#include "Typedefs.h"
|
||||
|
||||
|
|
|
@ -359,5 +359,5 @@ QString Torrent::getError() const
|
|||
|
||||
QPixmap TrackerStat::getFavicon() const
|
||||
{
|
||||
return trApp->faviconCache().find(sitename);
|
||||
return trApp->find_favicon(sitename);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
#include <libtransmission/quark.h>
|
||||
#include <libtransmission/tr-macros.h>
|
||||
|
||||
#include "FaviconCache.h"
|
||||
#include "IconCache.h"
|
||||
#include "Speed.h"
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
#include <QTextDocument>
|
||||
|
||||
#include <libtransmission/web-utils.h>
|
||||
#include <libtransmission/favicon-cache.h>
|
||||
|
||||
#include "FaviconCache.h"
|
||||
#include "Formatter.h"
|
||||
#include "Torrent.h"
|
||||
#include "TrackerDelegate.h"
|
||||
|
@ -58,7 +58,7 @@ ItemLayout::ItemLayout(
|
|||
QPoint const& top_left,
|
||||
int width)
|
||||
{
|
||||
QSize const icon_size = FaviconCache::getIconSize();
|
||||
auto const icon_size = QSize{ FaviconCache<QPixmap>::Width, FaviconCache<QPixmap>::Height };
|
||||
|
||||
QRect base_rect(top_left, QSize(width, 0));
|
||||
|
||||
|
|
|
@ -180,7 +180,7 @@ bool change(TrackerStat& setme, tr_variant const* value)
|
|||
if (site_changed && !setme.sitename.isEmpty() && !setme.announce.isEmpty())
|
||||
{
|
||||
setme.announce = trApp->intern(setme.announce);
|
||||
trApp->faviconCache().add(setme.sitename, setme.announce);
|
||||
trApp->load_favicon(setme.announce);
|
||||
}
|
||||
|
||||
return changed;
|
||||
|
|
Loading…
Reference in a new issue