Use Qt's xdg-desktop-portal file dialog implementation

This commit is contained in:
Ilya Fedin 2022-05-31 11:16:25 +04:00 committed by John Preston
parent 762f561c60
commit 391a3a77f6
6 changed files with 4 additions and 798 deletions

View File

@ -989,8 +989,6 @@ PRIVATE
platform/linux/linux_wayland_integration_dummy.cpp
platform/linux/linux_wayland_integration.cpp
platform/linux/linux_wayland_integration.h
platform/linux/linux_xdp_file_dialog.cpp
platform/linux/linux_xdp_file_dialog.h
platform/linux/linux_xdp_open_with_dialog.cpp
platform/linux/linux_xdp_open_with_dialog.h
platform/linux/file_utilities_linux.cpp
@ -1310,8 +1308,6 @@ endif()
if (DESKTOP_APP_DISABLE_DBUS_INTEGRATION)
remove_target_sources(Telegram ${src_loc}
platform/linux/linux_xdp_file_dialog.cpp
platform/linux/linux_xdp_file_dialog.h
platform/linux/linux_xdp_open_with_dialog.cpp
platform/linux/linux_xdp_open_with_dialog.h
platform/linux/notifications_manager_linux.cpp

View File

@ -8,7 +8,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "platform/linux/file_utilities_linux.h"
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
#include "platform/linux/linux_xdp_file_dialog.h"
#include "platform/linux/linux_xdp_open_with_dialog.h"
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
@ -84,22 +83,11 @@ bool Get(
if (parent) {
parent = parent->window();
}
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
{
const auto result = XDP::Get(
parent,
files,
remoteContent,
caption,
filter,
type,
startFile);
if (result.has_value()) {
return *result;
}
// Workaround for sandboxed paths
static const auto docRegExp = QRegularExpression("^/run/user/\\d+/doc");
if (cDialogLastPath().contains(docRegExp)) {
InitLastPath();
}
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
return ::FileDialog::internal::GetDefault(
parent,
files,

View File

@ -1,743 +0,0 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "platform/linux/linux_xdp_file_dialog.h"
#include "platform/platform_file_utilities.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/linux/base_linux_glibmm_helper.h"
#include "base/platform/linux/base_linux_xdp_utilities.h"
#include "storage/localstorage.h"
#include "base/random.h"
#include <QtGui/QWindow>
#include <QtWidgets/QFileDialog>
#include <glibmm.h>
#include <giomm.h>
namespace Platform {
namespace FileDialog {
namespace XDP {
namespace {
using Type = ::FileDialog::internal::Type;
constexpr auto kXDPFileChooserInterface = "org.freedesktop.portal.FileChooser"_cs;
constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"_cs;
const char *filterRegExp =
"^(.*)\\(([a-zA-Z0-9_.,*? +;#\\-\\[\\]@\\{\\}/!<>\\$%&=^~:\\|]*)\\)$";
std::optional<uint> FileChooserPortalVersion;
auto QStringListToStd(const QStringList &list) {
std::vector<Glib::ustring> result;
ranges::transform(
list,
ranges::back_inserter(result),
&QString::toStdString);
return result;
}
auto MakeFilterList(const QString &filter) {
std::vector<Glib::ustring> result;
QString f(filter);
if (f.isEmpty()) {
return result;
}
QString sep(QLatin1String(";;"));
int i = f.indexOf(sep, 0);
if (i == -1) {
if (f.indexOf(QLatin1Char('\n'), 0) != -1) {
sep = QLatin1Char('\n');
i = f.indexOf(sep, 0);
}
}
ranges::transform(
f.split(sep),
ranges::back_inserter(result),
[](const QString &string) {
return string.simplified().toStdString();
});
return result;
}
void ComputeFileChooserPortalVersion() {
const auto connection = [] {
try {
return Gio::DBus::Connection::get_sync(
Gio::DBus::BusType::BUS_TYPE_SESSION);
} catch (...) {
return Glib::RefPtr<Gio::DBus::Connection>();
}
}();
if (!connection) {
return;
}
connection->call(
std::string(base::Platform::XDP::kObjectPath),
std::string(kPropertiesInterface),
"Get",
base::Platform::MakeGlibVariant(std::tuple{
Glib::ustring(
std::string(kXDPFileChooserInterface)),
Glib::ustring("version"),
}),
[=](const Glib::RefPtr<Gio::AsyncResult> &result) {
try {
auto reply = connection->call_finish(result);
FileChooserPortalVersion =
base::Platform::GlibVariantCast<uint>(
base::Platform::GlibVariantCast<Glib::VariantBase>(
reply.get_child(0)));
} catch (const Glib::Error &e) {
static const auto NotSupportedErrors = {
"org.freedesktop.DBus.Error.ServiceUnknown",
};
const auto errorName =
Gio::DBus::ErrorUtils::get_remote_error(e);
if (!ranges::contains(NotSupportedErrors, errorName)) {
LOG(("XDP File Dialog Error: %1").arg(
QString::fromStdString(e.what())));
}
} catch (const std::exception &e) {
LOG(("XDP File Dialog Error: %1").arg(
QString::fromStdString(e.what())));
}
},
std::string(base::Platform::XDP::kService));
}
// This is a patched copy of file dialog from qxdgdesktopportal theme plugin.
// It allows using XDP file dialog flexibly,
// without relying on QT_QPA_PLATFORMTHEME variable.
//
// XDP file dialog is a dialog obtained via a DBus service
// provided by the current desktop environment.
class XDPFileDialog : public QDialog {
public:
enum ConditionType : uint {
GlobalPattern = 0,
MimeType = 1
};
// Filters a(sa(us))
// Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]
typedef std::tuple<uint, Glib::ustring> FilterCondition;
typedef std::vector<FilterCondition> FilterConditionList;
typedef std::tuple<Glib::ustring, FilterConditionList> Filter;
typedef std::vector<Filter> FilterList;
XDPFileDialog(
QWidget *parent = nullptr,
const QString &caption = QString(),
const QString &directory = QString(),
const QString &nameFilter = QString(),
const QStringList &mimeTypeFilters = QStringList());
~XDPFileDialog();
void setVisible(bool visible) override;
void setWindowTitle(const QString &windowTitle) {
_windowTitle = windowTitle.toStdString();
}
void setAcceptLabel(const QString &acceptLabel) {
_acceptLabel = acceptLabel.toStdString();
}
void setAcceptMode(QFileDialog::AcceptMode acceptMode) {
_acceptMode = acceptMode;
}
void setFileMode(QFileDialog::FileMode fileMode) {
_fileMode = fileMode;
}
void setOption(QFileDialog::Option option, bool on = true) {
if (on) {
_options |= option;
} else {
_options &= ~option;
}
}
QUrl directory() const;
void setDirectory(const QUrl &directory);
void selectFile(const QUrl &filename);
QList<QUrl> selectedFiles() const;
int exec() override;
bool failedToOpen() const {
return _failedToOpen;
}
private:
void openPortal();
void gotResponse(
uint response,
const std::map<Glib::ustring, Glib::VariantBase> &results);
void showHelper(
Qt::WindowFlags windowFlags,
Qt::WindowModality windowModality,
QWindow *parent);
void hideHelper();
rpl::producer<> accepted();
rpl::producer<> rejected();
Glib::RefPtr<Gio::DBus::Connection> _dbusConnection;
uint _requestSignalId = 0;
// Options
QWindow *_parent = nullptr;
QFileDialog::Options _options;
QFileDialog::AcceptMode _acceptMode = QFileDialog::AcceptOpen;
QFileDialog::FileMode _fileMode = QFileDialog::ExistingFile;
bool _modal = false;
Glib::ustring _windowTitle = "Choose file";
Glib::ustring _acceptLabel;
Glib::ustring _directory;
std::vector<Glib::ustring> _nameFilters;
std::vector<Glib::ustring> _mimeTypesFilters;
// maps user-visible name for portal to full name filter
std::map<Glib::ustring, Glib::ustring> _userVisibleToNameFilter;
Glib::ustring _selectedMimeTypeFilter;
Glib::ustring _selectedNameFilter;
std::vector<Glib::ustring> _selectedFiles;
bool _failedToOpen = false;
rpl::event_stream<> _accept;
rpl::event_stream<> _reject;
rpl::lifetime _lifetime;
};
XDPFileDialog::XDPFileDialog(
QWidget *parent,
const QString &caption,
const QString &directory,
const QString &nameFilter,
const QStringList &mimeTypeFilters)
: QDialog(parent)
, _windowTitle(caption.toStdString())
, _directory(directory.toStdString())
, _nameFilters(MakeFilterList(nameFilter))
, _mimeTypesFilters(QStringListToStd(mimeTypeFilters))
, _selectedMimeTypeFilter(!_mimeTypesFilters.empty()
? _mimeTypesFilters[0]
: Glib::ustring())
, _selectedNameFilter(!_nameFilters.empty()
? _nameFilters[0]
: Glib::ustring()) {
accepted(
) | rpl::start_with_next([=] {
accept();
}, _lifetime);
rejected(
) | rpl::start_with_next([=] {
reject();
}, _lifetime);
}
XDPFileDialog::~XDPFileDialog() {
if (_dbusConnection && _requestSignalId != 0) {
_dbusConnection->signal_unsubscribe(_requestSignalId);
}
}
void XDPFileDialog::openPortal() {
std::map<Glib::ustring, Glib::VariantBase> options;
if (!_acceptLabel.empty()) {
options["accept_label"] = Glib::Variant<Glib::ustring>::create(
_acceptLabel);
}
options["modal"] = Glib::Variant<bool>::create(_modal);
options["multiple"] = Glib::Variant<bool>::create(
_fileMode == QFileDialog::ExistingFiles);
options["directory"] = Glib::Variant<bool>::create(
_fileMode == QFileDialog::Directory
|| _options.testFlag(QFileDialog::ShowDirsOnly));
if (_acceptMode == QFileDialog::AcceptSave) {
if (!_directory.empty()) {
options["current_folder"] = Glib::Variant<std::string>::create(
_directory + '\0');
}
if (!_selectedFiles.empty()) {
options["current_file"] = Glib::Variant<std::string>::create(
_selectedFiles[0] + '\0');
options["current_name"] = Glib::Variant<Glib::ustring>::create(
Glib::path_get_basename(_selectedFiles[0]));
}
}
// Insert filters
FilterList filterList;
auto selectedFilterIndex = filterList.size() - 1;
_userVisibleToNameFilter.clear();
if (!_mimeTypesFilters.empty()) {
for (const auto &mimeTypeFilter : _mimeTypesFilters) {
auto mimeTypeUncertain = false;
const auto mimeType = Gio::content_type_guess(
mimeTypeFilter,
nullptr,
0,
mimeTypeUncertain);
// Creates e.g. (1, "image/png")
const auto filterCondition = FilterCondition{
MimeType,
mimeTypeFilter,
};
// Creates e.g. [("Images", [((1, "image/png"))])]
filterList.push_back({
Gio::content_type_get_description(mimeType),
FilterConditionList{filterCondition},
});
if (!_selectedMimeTypeFilter.empty()
&& _selectedMimeTypeFilter == mimeTypeFilter) {
selectedFilterIndex = filterList.size() - 1;
}
}
} else if (!_nameFilters.empty()) {
for (const auto &nameFilter : _nameFilters) {
// Do parsing:
// Supported format is ("Images (*.png *.jpg)")
const auto regexp = Glib::Regex::create(filterRegExp);
Glib::MatchInfo match;
regexp->match(nameFilter, match);
if (match.matches()) {
const auto userVisibleName = match.fetch(1);
const auto filterStrings = Glib::Regex::create(" ")->split(
match.fetch(2),
Glib::RegexMatchFlags::REGEX_MATCH_NOTEMPTY);
if (filterStrings.empty()) {
LOG((
"XDP File Dialog Error: "
"Filter %1 is empty and will be ignored.")
.arg(QString::fromStdString(userVisibleName)));
continue;
}
FilterConditionList filterConditions;
for (const auto &filterString : filterStrings) {
filterConditions.push_back({
GlobalPattern,
filterString,
});
}
filterList.push_back({
userVisibleName,
filterConditions,
});
_userVisibleToNameFilter[userVisibleName] = nameFilter;
if (!_selectedNameFilter.empty()
&& _selectedNameFilter == nameFilter) {
selectedFilterIndex = filterList.size() - 1;
}
}
}
}
if (!filterList.empty()) {
options["filters"] = Glib::Variant<FilterList>::create(filterList);
}
if (selectedFilterIndex != -1) {
options["current_filter"] = Glib::Variant<Filter>::create(
filterList[selectedFilterIndex]);
}
const auto handleToken = Glib::ustring("tdesktop")
+ std::to_string(base::RandomValue<uint>());
options["handle_token"] = Glib::Variant<Glib::ustring>::create(
handleToken);
// TODO choices a(ssa(ss)s)
// List of serialized combo boxes to add to the file chooser.
try {
_dbusConnection = Gio::DBus::Connection::get_sync(
Gio::DBus::BusType::BUS_TYPE_SESSION);
auto uniqueName = _dbusConnection->get_unique_name();
uniqueName.erase(0, 1);
uniqueName.replace(uniqueName.find('.'), 1, 1, '_');
const auto requestPath = Glib::ustring(
"/org/freedesktop/portal/desktop/request/")
+ uniqueName
+ '/'
+ handleToken;
const auto responseCallback = crl::guard(this, [=](
const Glib::RefPtr<Gio::DBus::Connection> &connection,
const Glib::ustring &sender_name,
const Glib::ustring &object_path,
const Glib::ustring &interface_name,
const Glib::ustring &signal_name,
const Glib::VariantContainerBase &parameters) {
try {
auto parametersCopy = parameters;
const auto response = base::Platform::GlibVariantCast<uint>(
parametersCopy.get_child(0));
const auto results = base::Platform::GlibVariantCast<
std::map<
Glib::ustring,
Glib::VariantBase
>>(parametersCopy.get_child(1));
gotResponse(response, results);
} catch (const std::exception &e) {
LOG(("XDP File Dialog Error: %1").arg(
QString::fromStdString(e.what())));
_reject.fire({});
}
});
_requestSignalId = _dbusConnection->signal_subscribe(
responseCallback,
{},
"org.freedesktop.portal.Request",
"Response",
requestPath);
_dbusConnection->call(
std::string(base::Platform::XDP::kObjectPath),
std::string(kXDPFileChooserInterface),
_acceptMode == QFileDialog::AcceptSave
? "SaveFile"
: "OpenFile",
base::Platform::MakeGlibVariant(std::tuple{
base::Platform::XDP::ParentWindowID(_parent),
_windowTitle,
options,
}),
crl::guard(this, [=](const Glib::RefPtr<Gio::AsyncResult> &result) {
try {
auto reply = _dbusConnection->call_finish(result);
const auto handle = base::Platform::GlibVariantCast<
Glib::ustring>(reply.get_child(0));
if (handle != requestPath) {
_dbusConnection->signal_unsubscribe(
_requestSignalId);
_requestSignalId = _dbusConnection->signal_subscribe(
responseCallback,
{},
"org.freedesktop.portal.Request",
"Response",
handle);
}
} catch (const Glib::Error &e) {
static const auto NotSupportedErrors = {
"org.freedesktop.DBus.Error.ServiceUnknown",
};
const auto errorName =
Gio::DBus::ErrorUtils::get_remote_error(e);
if (!ranges::contains(NotSupportedErrors, errorName)) {
LOG(("XDP File Dialog Error: %1").arg(
QString::fromStdString(e.what())));
}
crl::on_main([=] {
_failedToOpen = true;
_reject.fire({});
});
} catch (const std::exception &e) {
LOG(("XDP File Dialog Error: %1").arg(
QString::fromStdString(e.what())));
crl::on_main([=] {
_failedToOpen = true;
_reject.fire({});
});
}
}),
std::string(base::Platform::XDP::kService));
} catch (...) {
_failedToOpen = true;
_reject.fire({});
}
}
void XDPFileDialog::setDirectory(const QUrl &directory) {
_directory = directory.path().toStdString();
}
QUrl XDPFileDialog::directory() const {
return QString::fromStdString(_directory);
}
void XDPFileDialog::selectFile(const QUrl &filename) {
_selectedFiles.push_back(filename.path().toStdString());
}
QList<QUrl> XDPFileDialog::selectedFiles() const {
QList<QUrl> files;
ranges::transform(
_selectedFiles,
ranges::back_inserter(files),
[](const Glib::ustring &string) {
return QUrl(QString::fromStdString(string));
});
return files;
}
int XDPFileDialog::exec() {
setAttribute(Qt::WA_DeleteOnClose, false);
bool wasShowModal = testAttribute(Qt::WA_ShowModal);
setAttribute(Qt::WA_ShowModal, true);
setResult(0);
show();
if (failedToOpen()) {
return result();
}
QPointer<QDialog> guard = this;
// HACK we have to avoid returning until we emit
// that the dialog was accepted or rejected
const auto loop = Glib::MainLoop::create();
rpl::lifetime lifetime;
accepted(
) | rpl::start_with_next([&] {
loop->quit();
}, lifetime);
rejected(
) | rpl::start_with_next([&] {
loop->quit();
}, lifetime);
loop->run();
if (guard.isNull()) {
return QDialog::Rejected;
}
setAttribute(Qt::WA_ShowModal, wasShowModal);
return result();
}
void XDPFileDialog::setVisible(bool visible) {
if (visible) {
if (testAttribute(Qt::WA_WState_ExplicitShowHide)
&& !testAttribute(Qt::WA_WState_Hidden)) {
return;
}
} else if (testAttribute(Qt::WA_WState_ExplicitShowHide)
&& testAttribute(Qt::WA_WState_Hidden)) {
return;
}
if (visible) {
showHelper(
windowFlags(),
windowModality(),
parentWidget()
? parentWidget()->windowHandle()
: nullptr);
} else {
hideHelper();
}
// Set WA_DontShowOnScreen so that QDialog::setVisible(visible) below
// updates the state correctly, but skips showing the non-native version:
setAttribute(Qt::WA_DontShowOnScreen);
QDialog::setVisible(visible);
}
void XDPFileDialog::hideHelper() {
}
void XDPFileDialog::showHelper(
Qt::WindowFlags windowFlags,
Qt::WindowModality windowModality,
QWindow *parent) {
_modal = windowModality != Qt::NonModal;
_parent = parent;
openPortal();
}
void XDPFileDialog::gotResponse(
uint response,
const std::map<Glib::ustring, Glib::VariantBase> &results) {
try {
if (!response) {
if (const auto i = results.find("uris"); i != end(results)) {
_selectedFiles = base::Platform::GlibVariantCast<
std::vector<Glib::ustring>>(i->second);
_directory = _selectedFiles.empty()
? Glib::ustring()
: Glib::ustring(
Glib::path_get_dirname(_selectedFiles.back()));
}
if (const auto i = results.find("current_filter");
i != end(results)) {
auto selectedFilter = base::Platform::GlibVariantCast<
Filter>(i->second);
if (!std::get<1>(selectedFilter).empty()
&& std::get<0>(
std::get<1>(selectedFilter)[0]) == MimeType) {
// s.a. XDPFileDialog::openPortal
// which basically does the inverse
_selectedMimeTypeFilter = std::get<1>(
std::get<1>(selectedFilter)[0]);
_selectedNameFilter.clear();
} else {
_selectedNameFilter =
_userVisibleToNameFilter[std::get<0>(selectedFilter)];
_selectedMimeTypeFilter.clear();
}
}
_accept.fire({});
} else {
_reject.fire({});
}
} catch (const std::exception &e) {
LOG(("XDP File Dialog Error: %1").arg(
QString::fromStdString(e.what())));
_reject.fire({});
}
}
rpl::producer<> XDPFileDialog::accepted() {
return _accept.events();
}
rpl::producer<> XDPFileDialog::rejected() {
return _reject.events();
}
} // namespace
void Start() {
ComputeFileChooserPortalVersion();
}
std::optional<bool> Get(
QPointer<QWidget> parent,
QStringList &files,
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
Type type,
QString startFile) {
if (!FileChooserPortalVersion.has_value()
|| (type == Type::ReadFolder && *FileChooserPortalVersion < 3)) {
return std::nullopt;
}
static const auto docRegExp = QRegularExpression("^/run/user/\\d+/doc");
if (cDialogLastPath().isEmpty()
|| cDialogLastPath().contains(docRegExp)) {
InitLastPath();
}
XDPFileDialog dialog(parent, caption, QString(), filter, QStringList());
dialog.setModal(true);
if (type == Type::ReadFile || type == Type::ReadFiles) {
dialog.setFileMode((type == Type::ReadFiles)
? QFileDialog::ExistingFiles
: QFileDialog::ExistingFile);
dialog.setAcceptMode(QFileDialog::AcceptOpen);
} else if (type == Type::ReadFolder) {
dialog.setAcceptMode(QFileDialog::AcceptOpen);
dialog.setFileMode(QFileDialog::Directory);
dialog.setOption(QFileDialog::ShowDirsOnly);
} else {
dialog.setFileMode(QFileDialog::AnyFile);
dialog.setAcceptMode(QFileDialog::AcceptSave);
}
if (startFile.isEmpty() || startFile.at(0) != '/') {
startFile = cDialogLastPath() + '/' + startFile;
}
dialog.setDirectory(QFileInfo(startFile).absoluteDir().absolutePath());
dialog.selectFile(startFile);
const auto res = dialog.exec();
if (dialog.failedToOpen()) {
return std::nullopt;
}
if (type != Type::ReadFolder) {
// Save last used directory for all queries except directory choosing.
const auto path = dialog.directory().path();
if (!path.isEmpty()
&& !path.contains(docRegExp)
&& path != cDialogLastPath()) {
cSetDialogLastPath(path);
Local::writeSettings();
}
}
if (res == QDialog::Accepted) {
QStringList selectedFilesStrings;
ranges::transform(
dialog.selectedFiles(),
ranges::back_inserter(selectedFilesStrings),
[](const QUrl &url) { return url.path(); });
if (type == Type::ReadFiles) {
files = selectedFilesStrings;
} else {
files = selectedFilesStrings.mid(0, 1);
}
return true;
}
files = QStringList();
remoteContent = QByteArray();
return false;
}
} // namespace XDP
} // namespace FileDialog
} // namespace Platform

View File

@ -1,28 +0,0 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "core/file_utilities.h"
namespace Platform {
namespace FileDialog {
namespace XDP {
void Start();
std::optional<bool> Get(
QPointer<QWidget> parent,
QStringList &files,
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
::FileDialog::internal::Type type,
QString startFile);
} // namespace XDP
} // namespace FileDialog
} // namespace Platform

View File

@ -25,7 +25,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/platform/linux/base_linux_glibmm_helper.h"
#include "base/platform/linux/base_linux_dbus_utilities.h"
#include "base/platform/linux/base_linux_xdp_utilities.h"
#include "platform/linux/linux_xdp_file_dialog.h"
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
@ -630,10 +629,6 @@ namespace ThirdParty {
void start() {
LOG(("Icon theme: %1").arg(QIcon::themeName()));
LOG(("Fallback icon theme: %1").arg(QIcon::fallbackThemeName()));
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
FileDialog::XDP::Start();
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
}
void finish() {

View File

@ -385,8 +385,6 @@ parts:
- -./usr/libexec
- -./usr/mkspecs
- -./usr/modules
# Allow tdesktop's custom try-portal-and-fallback logic to work
- -./usr/plugins/platformthemes/libqxdgdesktopportal.so
after:
- mozjpeg
- patches