Bug#1115757: fragments: FTBFS: unsatisfiable build-dependencies: librust-ashpd-0.11+async-std-dev, librust-ashpd-0.11+gtk4-dev, librust-gtk4-0.9+default-dev, librust-gtk4-0.9+gnome-47-dev, librust-libadwaita-0.7+default-dev, librust-libadwaita-0.7+v1-7-dev
Peter Green
plugwash at debian.org
Thu Sep 25 16:10:07 BST 2025
- Previous message (by thread): Bug#1115757: fragments: FTBFS: unsatisfiable build-dependencies: librust-ashpd-0.11+async-std-dev, librust-ashpd-0.11+gtk4-dev, librust-gtk4-0.9+default-dev, librust-gtk4-0.9+gnome-47-dev, librust-libadwaita-0.7+default-dev, librust-libadwaita-0.7+v1-7-dev
- Next message (by thread): Bug#1115757: fragments: FTBFS: unsatisfiable build-dependencies: librust-ashpd-0.11+async-std-dev, librust-ashpd-0.11+gtk4-dev, librust-gtk4-0.9+default-dev, librust-gtk4-0.9+gnome-47-dev, librust-libadwaita-0.7+default-dev, librust-libadwaita-0.7+v1-7-dev
- Messages sorted by:
[ date ]
[ thread ]
[ subject ]
[ author ]
tags 1115757 +patch
thanks
> During a rebuild of all packages in sid, your package failed to build
> on amd64.
Attached is a debdiff that gets the package building again and also
prepares it for the upcoming update of rust-nix.
-------------- next part --------------
diff -Nru fragments-3.0.1/debian/changelog fragments-3.0.1/debian/changelog
--- fragments-3.0.1/debian/changelog 2025-04-01 12:07:19.000000000 +0000
+++ fragments-3.0.1/debian/changelog 2025-09-25 11:58:17.000000000 +0000
@@ -1,3 +1,10 @@
+fragments (3.0.1-8.1) UNRELEASED; urgency=medium
+
+ * Non-maintainer upload.
+ * Remove upper bound from nix dependency.
+
+ -- Peter Michael Green <plugwash at debian.org> Thu, 25 Sep 2025 11:58:17 +0000
+
fragments (3.0.1-8) unstable; urgency=high
* Salsa CI: Ignore reprotest failure
diff -Nru fragments-3.0.1/debian/control fragments-3.0.1/debian/control
--- fragments-3.0.1/debian/control 2025-04-01 12:07:19.000000000 +0000
+++ fragments-3.0.1/debian/control 2025-09-25 11:58:17.000000000 +0000
@@ -17,23 +17,23 @@
rustc:native,
libstd-rust-dev,
librust-anyhow-1+default-dev,
- librust-ashpd-0.11+async-std-dev,
- librust-ashpd-0.11+gtk4-dev,
+ librust-ashpd-0.12+async-std-dev,
+ librust-ashpd-0.12+gtk4-dev,
librust-async-process-2+default-dev (>= 2.2-~~),
librust-async-recursion-1+default-dev (>= 1.1-~~),
librust-base64-0.22+default-dev,
librust-futures-util-0.3+default-dev,
librust-gettext-rs-0.7+default-dev,
librust-gettext-rs-0.7+gettext-system-dev,
- librust-gtk4-0.9+default-dev,
- librust-gtk4-0.9+gnome-47-dev,
+ librust-gtk4-0.10+default-dev,
+ librust-gtk4-0.10+gnome-47-dev,
librust-human-sort-0.2+default-dev,
- librust-libadwaita-0.7+default-dev,
- librust-libadwaita-0.7+v1-7-dev,
+ librust-libadwaita-0.8+default-dev,
+ librust-libadwaita-0.8+v1-7-dev,
librust-log-0.4+default-dev,
librust-magnet-uri-0.2+default-dev,
- librust-nix-0.29+default-dev,
- librust-nix-0.29+signal-dev,
+ librust-nix+default-dev (>= 0.29),
+ librust-nix+signal-dev (>= 0.29),
librust-oo7-0.3+default-dev,
librust-pretty-env-logger-0.5+default-dev,
librust-regex-1+default-dev (>= 1.10-~~),
diff -Nru fragments-3.0.1/debian/patches/add-connection-dialog-cleanup.patch fragments-3.0.1/debian/patches/add-connection-dialog-cleanup.patch
--- fragments-3.0.1/debian/patches/add-connection-dialog-cleanup.patch 1970-01-01 00:00:00.000000000 +0000
+++ fragments-3.0.1/debian/patches/add-connection-dialog-cleanup.patch 2025-09-25 11:58:17.000000000 +0000
@@ -0,0 +1,229 @@
+commit b90a5be8afe7f4fcc29e5f1ff6b853bf907fbb3a
+Author: Felix Häcker <haeckerfelix at gnome.org>
+Date: Sun Nov 12 22:29:44 2023 +0100
+
+ add_connection_dialog: Code cleanup
+
+diff --git a/src/ui/add_connection_dialog.rs b/src/ui/add_connection_dialog.rs
+index fc02546..f3cd6ab 100644
+--- a/src/ui/add_connection_dialog.rs
++++ b/src/ui/add_connection_dialog.rs
+@@ -64,7 +64,7 @@ mod imp {
+
+ fn class_init(klass: &mut Self::Class) {
+ klass.bind_template();
+- Self::Type::bind_template_callbacks(klass);
++ klass.bind_template_callbacks();
+ }
+
+ fn instance_init(obj: &subclass::InitializingObject<Self>) {
+@@ -81,119 +81,114 @@ mod imp {
+ impl AdwWindowImpl for FrgAddConnectionDialog {}
+
+ impl DialogImpl for FrgAddConnectionDialog {}
+-}
+
+-glib::wrapper! {
+- pub struct FrgAddConnectionDialog(ObjectSubclass<imp::FrgAddConnectionDialog>)
+- @extends gtk::Widget, gtk::Window, adw::Window;
+-}
++ #[gtk::template_callbacks]
++ impl FrgAddConnectionDialog {
++ fn add_connection(&self, connection: FrgConnection) {
++ let cm = FrgConnectionManager::default();
++ let app = FrgApplication::default();
+
+-#[gtk::template_callbacks]
+-impl FrgAddConnectionDialog {
+- pub fn new() -> Self {
+- Self::default()
+- }
+-
+- fn add_connection(&self, connection: FrgConnection) {
+- let cm = FrgConnectionManager::default();
+- let app = FrgApplication::default();
+-
+- cm.connections().append(&connection);
+- app.activate_action("set-connection", Some(&connection.uuid().to_variant()));
+- self.close();
+- }
++ cm.connections().append(&connection);
++ app.activate_action("set-connection", Some(&connection.uuid().to_variant()));
++ self.close();
++ }
+
+- #[template_callback]
+- fn connect_button_clicked(&self) {
+- let fut = clone!(@weak self as this => async move {
+- let imp = this.imp();
+- let title = imp.title_row.text();
+- let address = imp.address.borrow().clone();
+-
+- let connection = FrgConnection::new(&title, &address);
+-
+- imp.stack.set_visible_child_name("loading");
+- imp.spinner.set_spinning(true);
+- imp.connect_button.set_sensitive(false);
+-
+- let res = TrClient::test_connectivity(connection.address(), None).await;
+- match res{
+- Ok(_) => this.add_connection(connection),
+- Err(ref err) => {
+- // Skip "Unauthorized" errors since we can handle those
+- if matches!(err, ClientError::TransmissionUnauthorized) {
+- this.add_connection(connection);
+- } else {
+- let msg = i18n_f("Could not connect with “{}”:\n{}", &[&imp.address.borrow(), &err.to_string()]);
+-
+- imp.error_label.set_visible(true);
+- imp.error_label.set_text(&msg);
+-
+- imp.stack.set_visible_child_name("input");
+- imp.spinner.set_spinning(false);
+- imp.connect_button.set_sensitive(true);
++ #[template_callback]
++ fn connect_button_clicked(&self) {
++ let fut = clone!(@weak self as this => async move {
++ let title = this.title_row.text();
++ let address = this.address.borrow().clone();
++
++ let connection = FrgConnection::new(&title, &address);
++
++ this.stack.set_visible_child_name("loading");
++ this.spinner.set_spinning(true);
++ this.connect_button.set_sensitive(false);
++
++ let res = TrClient::test_connectivity(connection.address(), None).await;
++ match res {
++ Ok(_) => this.add_connection(connection),
++ Err(ref err) => {
++ // Skip "Unauthorized" errors since we can handle those
++ if matches!(err, ClientError::TransmissionUnauthorized) {
++ this.add_connection(connection);
++ } else {
++ let msg = i18n_f("Could not connect with “{}”:\n{}", &[&this.address.borrow(), &err.to_string()]);
++
++ this.error_label.set_visible(true);
++ this.error_label.set_text(&msg);
++
++ this.stack.set_visible_child_name("input");
++ this.spinner.set_spinning(false);
++ this.connect_button.set_sensitive(true);
++ }
+ }
+ }
+- }
+- });
+- spawn!(fut);
+- }
++ });
++ spawn!(fut);
++ }
+
+- #[template_callback]
+- fn validate_title(&self) {
+- let imp = self.imp();
++ #[template_callback]
++ fn validate_title(&self) {
++ if self.title_row.text().is_empty() {
++ self.title_row.add_css_class("error");
++ self.title_ok.set(false);
++ } else {
++ self.title_row.remove_css_class("error");
++ self.title_ok.set(true);
++ }
+
+- if imp.title_row.text().is_empty() {
+- imp.title_row.add_css_class("error");
+- imp.title_ok.set(false);
+- } else {
+- imp.title_row.remove_css_class("error");
+- imp.title_ok.set(true);
++ self.update_connect_button();
+ }
+
+- self.update_connect_button();
+- }
++ #[template_callback]
++ fn validate_address(&self) {
++ let host = self.host_row.text();
++ let port = self.port_spinbutton.value();
++ let path = self.path_row.text();
++ let is_ssl = self.ssl_switch.is_active();
++
++ let address = if is_ssl {
++ format!("https://{host}:{port}{path}")
++ } else {
++ format!("http://{host}:{port}{path}")
++ };
++ *self.address.borrow_mut() = address.to_string();
+
+- #[template_callback]
+- fn validate_address(&self) {
+- let imp = self.imp();
+-
+- let host = imp.host_row.text();
+- let port = imp.port_spinbutton.value();
+- let path = imp.path_row.text();
+- let is_ssl = imp.ssl_switch.is_active();
+-
+- let address = if is_ssl {
+- format!("https://{host}:{port}{path}")
+- } else {
+- format!("http://{host}:{port}{path}")
+- };
+- *imp.address.borrow_mut() = address.to_string();
+-
+- let valid_address = url::Url::parse(&address).is_ok();
+-
+- if !imp.host_row.text().is_empty() && valid_address {
+- imp.host_row.remove_css_class("error");
+- imp.address_ok.set(true);
+- } else {
+- // Don't recolor entry if it is empty
+- if !imp.host_row.text().is_empty() {
+- imp.host_row.add_css_class("error");
++ let valid_address = url::Url::parse(&address).is_ok();
++
++ if !self.host_row.text().is_empty() && valid_address {
++ self.host_row.remove_css_class("error");
++ self.address_ok.set(true);
+ } else {
+- imp.host_row.remove_css_class("error");
++ // Don't recolor entry if it is empty
++ if !self.host_row.text().is_empty() {
++ self.host_row.add_css_class("error");
++ } else {
++ self.host_row.remove_css_class("error");
++ }
++
++ self.address_ok.set(false);
+ }
+
+- imp.address_ok.set(false);
++ self.update_connect_button();
+ }
+
+- self.update_connect_button();
++ fn update_connect_button(&self) {
++ let sensitive = self.title_ok.get() && self.address_ok.get();
++ self.connect_button.set_sensitive(sensitive);
++ }
+ }
++}
+
+- fn update_connect_button(&self) {
+- let imp = self.imp();
++glib::wrapper! {
++ pub struct FrgAddConnectionDialog(ObjectSubclass<imp::FrgAddConnectionDialog>)
++ @extends gtk::Widget, gtk::Window, adw::Window;
++}
+
+- let sensitive = imp.title_ok.get() && imp.address_ok.get();
+- imp.connect_button.set_sensitive(sensitive);
++impl FrgAddConnectionDialog {
++ pub fn new() -> Self {
++ Self::default()
+ }
+ }
+
diff -Nru fragments-3.0.1/debian/patches/cargo-Update-nix-crate.patch fragments-3.0.1/debian/patches/cargo-Update-nix-crate.patch
--- fragments-3.0.1/debian/patches/cargo-Update-nix-crate.patch 2025-04-01 12:07:19.000000000 +0000
+++ fragments-3.0.1/debian/patches/cargo-Update-nix-crate.patch 2025-09-25 11:56:26.000000000 +0000
@@ -28,7 +28,7 @@
log = "0.4"
magnet-uri = "0.2"
-nix = { version = "0.28", features = ["signal"] }
-+nix = { version = "0.29", features = ["signal"] }
++nix = { version = ">= 0.29", features = ["signal"] }
once_cell = "1.19"
pretty_env_logger = "0.5"
regex = "1.10"
diff -Nru fragments-3.0.1/debian/patches/misc-Drop-once-cell-crate.patch fragments-3.0.1/debian/patches/misc-Drop-once-cell-crate.patch
--- fragments-3.0.1/debian/patches/misc-Drop-once-cell-crate.patch 2025-04-01 12:07:19.000000000 +0000
+++ fragments-3.0.1/debian/patches/misc-Drop-once-cell-crate.patch 2025-09-25 11:57:59.000000000 +0000
@@ -15,14 +15,8 @@
index c7365c0..f531309 100644
--- a/Cargo.toml
+++ b/Cargo.toml
-@@ -17,7 +17,6 @@ human-sort = "0.2"
- log = "0.4"
- magnet-uri = "0.2"
- nix = { version = "0.29", features = ["signal"] }
+@@ -20,1 +20,0 @@
-once_cell = "1.19"
- pretty_env_logger = "0.5"
- regex = "1.10"
- oo7 = "0.3"
diff --git a/src/backend/connection_manager.rs b/src/backend/connection_manager.rs
index a9713f9..d747aeb 100644
--- a/src/backend/connection_manager.rs
diff -Nru fragments-3.0.1/debian/patches/series fragments-3.0.1/debian/patches/series
--- fragments-3.0.1/debian/patches/series 2025-04-01 12:07:19.000000000 +0000
+++ fragments-3.0.1/debian/patches/series 2025-09-25 11:58:17.000000000 +0000
@@ -8,3 +8,4 @@
stats_dialog-Use-AdwInlineViewSwitcher.patch
cargo-Update-to-adw-1.7.patch
cargo-Update-to-ashpd-0.11.patch
+update-gtk-crates.patch
diff -Nru fragments-3.0.1/debian/patches/update-gtk-crates.patch fragments-3.0.1/debian/patches/update-gtk-crates.patch
--- fragments-3.0.1/debian/patches/update-gtk-crates.patch 1970-01-01 00:00:00.000000000 +0000
+++ fragments-3.0.1/debian/patches/update-gtk-crates.patch 2025-09-25 11:58:17.000000000 +0000
@@ -0,0 +1,2938 @@
+This patch is based on the upsteram commits described below adapted
+for use in the Debian package by Peter Michael Green.
+
+commit 27f81842a1c1f8a3178e3a006570b48b5b7c938b
+Author: Maximiliano Sandoval <msandova at gnome.org>
+Date: Sun Jul 14 11:20:03 2024 +0200
+
+ Use adw::Spinner
+
+commit b2cbf3dbc1b15bb507f68aeeef9cb6c76b2cbe80
+Author: Felix Häcker <haeckerfelix at gnome.org>
+Date: Sun Aug 3 14:53:48 2025 +0200
+
+ misc: Upgrade gtk-rs to 0.21
+
+commit 89296ea1ae3c4633f697f120c4e77e32a693a5f8
+Author: Maximiliano Sandoval <msandova at gnome.org>
+Date: Sun Jul 14 12:58:35 2024 +0200
+
+ Port to new clone macro
+
+Index: fragments-3.0.1/data/gtk/add_connection_dialog.ui
+===================================================================
+--- fragments-3.0.1.orig/data/gtk/add_connection_dialog.ui
++++ fragments-3.0.1/data/gtk/add_connection_dialog.ui
+@@ -133,10 +133,7 @@
+ <object class="GtkStackPage">
+ <property name="name">loading</property>
+ <property name="child">
+- <object class="GtkSpinner" id="spinner">
+- <property name="halign">center</property>
+- <property name="valign">center</property>
+- </object>
++ <object class="AdwSpinner" />
+ </property>
+ </object>
+ </child>
+Index: fragments-3.0.1/data/gtk/connection_box.ui
+===================================================================
+--- fragments-3.0.1.orig/data/gtk/connection_box.ui
++++ fragments-3.0.1/data/gtk/connection_box.ui
+@@ -25,10 +25,7 @@
+ <object class="GtkStackPage">
+ <property name="name">loading</property>
+ <property name="child">
+- <object class="GtkSpinner" id="spinner">
+- <property name="halign">center</property>
+- <property name="width-request">32</property>
+- </object>
++ <object class="AdwSpinner" />
+ </property>
+ </object>
+ </child>
+Index: fragments-3.0.1/data/gtk/torrent_row.ui
+===================================================================
+--- fragments-3.0.1.orig/data/gtk/torrent_row.ui
++++ fragments-3.0.1/data/gtk/torrent_row.ui
+@@ -50,9 +50,7 @@
+ <object class="GtkStackPage">
+ <property name="name">check</property>
+ <property name="child">
+- <object class="GtkSpinner" id="spinner">
+- <property name="halign">center</property>
+- <property name="valign">center</property>
++ <object class="AdwSpinner">
+ <style>
+ <class name="bubble"/>
+ </style>
+Index: fragments-3.0.1/src/ui/add_connection_dialog.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/add_connection_dialog.rs
++++ fragments-3.0.1/src/ui/add_connection_dialog.rs
+@@ -1,5 +1,5 @@
+ // Fragments - add_connection_dialog.rs
+-// Copyright (C) 2022-2023 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -48,8 +48,6 @@ mod imp {
+ connect_button: TemplateChild<gtk::Button>,
+ #[template_child]
+ error_label: TemplateChild<gtk::Label>,
+- #[template_child]
+- spinner: TemplateChild<gtk::Spinner>,
+
+ title_ok: Cell<bool>,
+ address_ok: Cell<bool>,
+@@ -91,36 +89,41 @@ mod imp {
+
+ #[template_callback]
+ fn connect_button_clicked(&self) {
+- let fut = clone!(@weak self as this => async move {
+- let title = this.title_row.text();
+- let address = this.address.borrow().clone();
+-
+- let connection = FrgConnection::new(&title, &address);
+-
+- this.stack.set_visible_child_name("loading");
+- this.spinner.set_spinning(true);
+- this.connect_button.set_sensitive(false);
+-
+- let res = TrClient::test_connectivity(connection.address(), None).await;
+- match res {
+- Ok(_) => this.add_connection(connection),
+- Err(ref err) => {
+- // Skip "Unauthorized" errors since we can handle those
+- if matches!(err, ClientError::TransmissionUnauthorized) {
+- this.add_connection(connection);
+- } else {
+- let msg = i18n_f("Could not connect with “{}”:\n{}", &[&this.address.borrow(), &err.to_string()]);
+-
+- this.error_label.set_visible(true);
+- this.error_label.set_text(&msg);
+-
+- this.stack.set_visible_child_name("input");
+- this.spinner.set_spinning(false);
+- this.connect_button.set_sensitive(true);
++ let fut = clone!(
++ #[weak(rename_to = this)]
++ self,
++ async move {
++ let title = this.title_row.text();
++ let address = this.address.borrow().clone();
++
++ let connection = FrgConnection::new(&title, &address);
++
++ this.stack.set_visible_child_name("loading");
++ this.connect_button.set_sensitive(false);
++
++ let res = TrClient::test_connectivity(connection.address(), None).await;
++ match res {
++ Ok(_) => this.add_connection(connection),
++ Err(ref err) => {
++ // Skip "Unauthorized" errors since we can handle those
++ if matches!(err, ClientError::TransmissionUnauthorized) {
++ this.add_connection(connection);
++ } else {
++ let msg = i18n_f(
++ "Could not connect with “{}”:\n{}",
++ &[&this.address.borrow(), &err.to_string()],
++ );
++
++ this.error_label.set_visible(true);
++ this.error_label.set_text(&msg);
++
++ this.stack.set_visible_child_name("input");
++ this.connect_button.set_sensitive(true);
++ }
+ }
+ }
+ }
+- });
++ );
+ glib::spawn_future_local(fut);
+ }
+
+@@ -179,7 +182,8 @@ mod imp {
+
+ glib::wrapper! {
+ pub struct FrgAddConnectionDialog(ObjectSubclass<imp::FrgAddConnectionDialog>)
+- @extends gtk::Widget, adw::Dialog;
++ @extends gtk::Widget, adw::Dialog,
++ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ impl FrgAddConnectionDialog {
+Index: fragments-3.0.1/src/ui/connection_box.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/connection_box.rs
++++ fragments-3.0.1/src/ui/connection_box.rs
+@@ -1,5 +1,5 @@
+ // Fragments - connection_box.rs
+-// Copyright (C) 2022-2024 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -49,8 +49,6 @@ mod imp {
+ #[template_child]
+ pub torrent_page: TemplateChild<FrgTorrentPage>,
+ #[template_child]
+- spinner: TemplateChild<gtk::Spinner>,
+- #[template_child]
+ welcome_status_page: TemplateChild<adw::StatusPage>,
+ #[template_child]
+ failure_status_page: TemplateChild<adw::StatusPage>,
+@@ -99,51 +97,70 @@ mod imp {
+ self.set_view(View::Loading);
+
+ // Show a message when the currently configured directory is not accessible
+- cm.connect_invalid_dir_notify(clone!(@weak self as this => move |cm|{
+- let res: Option<String> = match cm.invalid_dir(){
+- FrgDirectoryType::Download => Some(
+- i18n("The configured download directory cannot be accessed."),
+- ),
+- FrgDirectoryType::Incomplete => Some(
+- i18n("The configured incomplete directory cannot be accessed."),
+- ),
+- _ => None,
+- };
++ cm.connect_invalid_dir_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |cm| {
++ let res: Option<String> = match cm.invalid_dir() {
++ FrgDirectoryType::Download => Some(i18n(
++ "The configured download directory cannot be accessed.",
++ )),
++ FrgDirectoryType::Incomplete => Some(i18n(
++ "The configured incomplete directory cannot be accessed.",
++ )),
++ _ => None,
++ };
+
+- if let Some(res) = res{
+- this.invalid_dir_banner.set_revealed(true);
+- this.invalid_dir_banner.set_title(&res);
+- }else{
+- this.invalid_dir_banner.set_revealed(false);
++ if let Some(res) = res {
++ this.invalid_dir_banner.set_revealed(true);
++ this.invalid_dir_banner.set_title(&res);
++ } else {
++ this.invalid_dir_banner.set_revealed(false);
++ }
+ }
+- }));
++ ));
+
+ cm.connect_local(
+ "daemon-started",
+ false,
+- clone!(@weak self as this => @default-panic, move |_| {
+- this.update_torrents_view();
+- None
+- }),
++ clone!(
++ #[weak(rename_to = this)]
++ self,
++ #[upgrade_or_panic]
++ move |_| {
++ this.update_torrents_view();
++ None
++ }
++ ),
+ );
+
+ cm.connect_local(
+ "daemon-stopped",
+ false,
+- clone!(@weak self as this => @default-panic, move |val| {
+- let reason = val[1].get::<FrgDaemonStopReason>().unwrap();
+- this.show_stop_reason(reason);
+- None
+- }),
++ clone!(
++ #[weak(rename_to = this)]
++ self,
++ #[upgrade_or_panic]
++ move |val| {
++ let reason = val[1].get::<FrgDaemonStopReason>().unwrap();
++ this.show_stop_reason(reason);
++ None
++ }
++ ),
+ );
+
+ client.torrents().connect_local(
+ "items-changed",
+ false,
+- clone!(@weak self as this => @default-panic, move |_| {
+- this.update_torrents_view();
+- None
+- }),
++ clone!(
++ #[weak(rename_to = this)]
++ self,
++ #[upgrade_or_panic]
++ move |_| {
++ this.update_torrents_view();
++ None
++ }
++ ),
+ );
+
+ client.connect_local("connection-failure", false, move |_| {
+@@ -165,31 +182,35 @@ mod imp {
+ let drop_target = self.drag_overlay.drop_target();
+ drop_target.set_types(&[gdk::FileList::static_type()]);
+
+- drop_target.connect_accept(clone!(@weak client => @default-return false, move |_, _| {
+- // Only accept drops when we're connected to a Transmission session
+- client.is_connected()
+- }));
+-
+- drop_target.connect_drop(
+- clone!(@weak cm => @default-return false, move |_, value, _, _| {
+- let files = match value.get::<gdk::FileList>() {
+- Ok(list) => list.files(),
+- Err(err) => {
+- error!("Issue with drop value: {err}");
+- return false;
+- }
+- };
++ drop_target.connect_accept(clone!(
++ #[weak]
++ client,
++ #[upgrade_or]
++ false,
++ move |_, _| {
++ // Only accept drops when we're connected to a Transmission session
++ client.is_connected()
++ }
++ ));
+
+- if !files.is_empty() {
+- let app = FrgApplication::default();
+- app.add_torrents_from_files(&files);
+- return true;
+- } else {
+- error!("Dropped FileList was empty");
++ drop_target.connect_drop(move |_, value, _, _| {
++ let files = match value.get::<gdk::FileList>() {
++ Ok(list) => list.files(),
++ Err(err) => {
++ error!("Issue with drop value: {err}");
+ return false;
+ }
+- }),
+- );
++ };
++
++ if !files.is_empty() {
++ let app = FrgApplication::default();
++ app.add_torrents_from_files(&files);
++ return true;
++ } else {
++ error!("Dropped FileList was empty");
++ return false;
++ }
++ });
+
+ self.stack.add_controller(drop_target.clone());
+ }
+@@ -236,8 +257,6 @@ mod imp {
+ }
+
+ pub fn set_view(&self, view: View) {
+- self.spinner.set_spinning(false);
+-
+ let name = match view {
+ View::Authentication => {
+ // Unset previous entry text
+@@ -245,10 +264,7 @@ mod imp {
+ self.password_row.set_text("");
+ "authentication"
+ }
+- View::Loading => {
+- self.spinner.set_spinning(true);
+- "loading"
+- }
++ View::Loading => "loading",
+ View::Ready => "ready",
+ View::Torrents => "torrents",
+ View::Metered => "metered",
+@@ -300,7 +316,8 @@ mod imp {
+ glib::wrapper! {
+ pub struct FrgConnectionBox(
+ ObjectSubclass<imp::FrgConnectionBox>)
+- @extends gtk::Widget, gtk::Box;
++ @extends gtk::Widget, gtk::Box,
++ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ impl FrgConnectionBox {
+Index: fragments-3.0.1/src/ui/torrent_row.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/torrent_row.rs
++++ fragments-3.0.1/src/ui/torrent_row.rs
+@@ -1,4 +1,5 @@
+-// Copyright (C) 2022-2024 Felix Häcker <haeckerfelix at gnome.org>
++// Fragments - torrent_row.rs
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -42,8 +43,6 @@ mod imp {
+ #[template_child]
+ progress_bar: TemplateChild<gtk::ProgressBar>,
+ #[template_child]
+- spinner: TemplateChild<gtk::Spinner>,
+- #[template_child]
+ queue_box: TemplateChild<gtk::Box>,
+ #[template_child]
+ index_stack: TemplateChild<gtk::Stack>,
+@@ -96,10 +95,14 @@ mod imp {
+
+ self.obj().torrent().connect_notify_local(
+ None,
+- clone!(@weak self as this => move |_, _| {
+- this.update_labels();
+- this.update_widgets();
+- }),
++ clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, _| {
++ this.update_labels();
++ this.update_widgets();
++ }
++ ),
+ );
+
+ // Shift+F10 / `Menu` button to open context menu
+@@ -107,12 +110,16 @@ mod imp {
+ gtk::KeyvalTrigger::new(gdk::Key::F10, gdk::ModifierType::SHIFT_MASK),
+ gtk::KeyvalTrigger::new(gdk::Key::Menu, gdk::ModifierType::empty()),
+ );
+- let action = gtk::CallbackAction::new(
+- clone!(@weak self as this => @default-return glib::Propagation::Stop, move |_, _| {
++ let action = gtk::CallbackAction::new(clone!(
++ #[weak(rename_to = this)]
++ self,
++ #[upgrade_or]
++ glib::Propagation::Stop,
++ move |_, _| {
+ this.show_context_menu(None::<>k::Gesture>, 40.0, 40.0);
+ glib::Propagation::Stop
+- }),
+- );
++ }
++ ));
+ let shortcut = gtk::Shortcut::new(Some(trigger), Some(action));
+
+ let controller = gtk::ShortcutController::new();
+@@ -122,17 +129,21 @@ mod imp {
+ // Right click to open context menu
+ let controller = gtk::GestureClick::new();
+ controller.set_button(gdk::BUTTON_SECONDARY);
+- controller.connect_pressed(
+- clone!(@weak self as this => move |c, _, x, y| this.show_context_menu(Some(c), x, y)),
+- );
++ controller.connect_pressed(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |c, _, x, y| this.show_context_menu(Some(c), x, y)
++ ));
+ self.obj().add_controller(controller);
+
+ // Touch long-press to open context menu
+ let controller = gtk::GestureLongPress::new();
+ controller.set_touch_only(true);
+- controller.connect_pressed(
+- clone!(@weak self as this => move |c, x, y| this.show_context_menu(Some(c), x, y)),
+- );
++ controller.connect_pressed(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |c, x, y| this.show_context_menu(Some(c), x, y)
++ ));
+ self.obj().add_controller(controller);
+
+ self.update_labels();
+@@ -183,7 +194,6 @@ mod imp {
+
+ fn update_widgets(&self) {
+ self.queue_box.set_visible(false);
+- self.spinner.set_spinning(false);
+
+ self.obj().remove_css_class("inactive");
+ self.description_label.remove_css_class("error");
+@@ -208,7 +218,6 @@ mod imp {
+ action_name = "pause".into();
+
+ self.obj().add_css_class("inactive");
+- self.spinner.set_spinning(true);
+ }
+ TrTorrentStatus::Stopped => {
+ index_name = "stopped".into();
+@@ -280,7 +289,8 @@ mod imp {
+ glib::wrapper! {
+ pub struct FrgTorrentRow(
+ ObjectSubclass<imp::FrgTorrentRow>)
+- @extends gtk::Widget, gtk::ListBoxRow;
++ @extends gtk::Widget, gtk::ListBoxRow,
++ @implements gtk::Actionable, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ impl FrgTorrentRow {
+Index: fragments-3.0.1/Cargo.toml
+===================================================================
+--- fragments-3.0.1.orig/Cargo.toml
++++ fragments-3.0.1/Cargo.toml
+@@ -7,7 +7,7 @@ license = "GPL-3.0-or-later"
+
+ [dependencies]
+ anyhow = "1.0"
+-ashpd = { version = "0.11", default-features=false, features = ["gtk4", "async-std"] }
++ashpd = { version = "0.12", default-features=false, features = ["gtk4", "async-std"] }
+ async-process = "2.2"
+ async-recursion = "1.1"
+ base64 = "0.22"
+@@ -28,8 +28,8 @@ strum_macros = "0.26"
+ url = "2.5"
+ uuid = { version = "1.10", features = ["v4"] }
+
+-gtk = { package = "gtk4", version = "0.9", features = ["gnome_47"] }
+-adw = { package = "libadwaita", version = "0.7", features = ["v1_7"] }
++gtk = { package = "gtk4", version = "0.10", features = ["gnome_47"] }
++adw = { package = "libadwaita", version = "0.8", features = ["v1_7"] }
+
+ [dependencies.transmission-gobject]
+ version = "0.1.6"
+Index: fragments-3.0.1/src/actions.rs
+===================================================================
+--- fragments-3.0.1.orig/src/actions.rs
++++ fragments-3.0.1/src/actions.rs
+@@ -1,5 +1,5 @@
+ // Fragments - actions.rs
+-// Copyright (C) 2023-2024 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2023-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -38,119 +38,195 @@ pub fn install_torrent_actions(torrent:
+
+ // torrent.continue
+ let continue_action = SimpleAction::new("continue", None);
+- continue_action.connect_activate(clone!(@weak torrent => move |_, _| {
+- let fut = async move {
+- torrent.start(false).await.expect("Unable to start torrent");
+- };
+- glib::spawn_future_local(fut);
+- }));
++ continue_action.connect_activate(clone!(
++ #[weak]
++ torrent,
++ move |_, _| {
++ let fut = async move {
++ torrent.start(false).await.expect("Unable to start torrent");
++ };
++ glib::spawn_future_local(fut);
++ }
++ ));
+ actions.add_action(&continue_action);
+
+ // torrent.pause
+ let pause_action = SimpleAction::new("pause", None);
+- pause_action.connect_activate(clone!(@weak torrent => move |_, _| {
+- let fut = async move {
+- torrent.stop().await.expect("Unable to stop torrent");
+- };
+- glib::spawn_future_local(fut);
+- }));
++ pause_action.connect_activate(clone!(
++ #[weak]
++ torrent,
++ move |_, _| {
++ let fut = async move {
++ torrent.stop().await.expect("Unable to stop torrent");
++ };
++ glib::spawn_future_local(fut);
++ }
++ ));
+ actions.add_action(&pause_action);
+
+ // Disable `continue`/`pause` action depending on the torrent status
+- torrent.connect_status_notify(
+- clone!(@weak torrent, @weak continue_action, @weak pause_action => move |_|{
++ torrent.connect_status_notify(clone!(
++ #[weak]
++ torrent,
++ #[weak]
++ continue_action,
++ #[weak]
++ pause_action,
++ move |_| {
+ update_status_action(&torrent, &continue_action, &pause_action);
+- }),
+- );
++ }
++ ));
+ update_status_action(torrent, &continue_action, &pause_action);
+
+ // torrent.remove
+ let remove_action = SimpleAction::new("remove", None);
+- remove_action.connect_activate(clone!(@weak torrent, @weak widget => move |_, _| {
+- show_remove_torrent_dialog(&torrent, widget);
+- }));
++ remove_action.connect_activate(clone!(
++ #[weak]
++ torrent,
++ #[weak]
++ widget,
++ move |_, _| {
++ show_remove_torrent_dialog(&torrent, widget);
++ }
++ ));
+ actions.add_action(&remove_action);
+
+ // torrent.copy-magnet
+ let copy_magnet_action = SimpleAction::new("copy-magnet", None);
+- copy_magnet_action.connect_activate(clone!(@weak torrent, @weak widget => move |_, _| {
+- let display = gdk::Display::default().unwrap();
+- let clipboard = display.clipboard();
+-
+- let content = gdk::ContentProvider::for_value(&torrent.magnet_link().to_value());
+- if let Err(err) = clipboard.set_content(Some(&content)){
+- utils::inapp_notification(&i18n("Unable to copy magnet link to clipboard"), Some(&widget));
+- warn!("Unable to copy magnet link to clipboard: {}", err.to_string());
+- }
++ copy_magnet_action.connect_activate(clone!(
++ #[weak]
++ torrent,
++ #[weak]
++ widget,
++ move |_, _| {
++ let display = gdk::Display::default().unwrap();
++ let clipboard = display.clipboard();
++
++ let content = gdk::ContentProvider::for_value(&torrent.magnet_link().to_value());
++ if let Err(err) = clipboard.set_content(Some(&content)) {
++ utils::inapp_notification(
++ &i18n("Unable to copy magnet link to clipboard"),
++ Some(&widget),
++ );
++ warn!(
++ "Unable to copy magnet link to clipboard: {}",
++ err.to_string()
++ );
++ }
+
+- utils::inapp_notification(&i18n("Copied magnet link to clipboard"), Some(&widget));
+- }));
++ utils::inapp_notification(&i18n("Copied magnet link to clipboard"), Some(&widget));
++ }
++ ));
+ actions.add_action(©_magnet_action);
+
+ // torrent.manual-update
+ let manual_update_action = SimpleAction::new("manual-update", None);
+- manual_update_action.connect_activate(clone!(@weak torrent => move |_, _| {
+- let fut = async move {
+- torrent.reannounce().await.expect("Unable to reannounce torrent");
+- };
+- glib::spawn_future_local(fut);
+- }));
++ manual_update_action.connect_activate(clone!(
++ #[weak]
++ torrent,
++ move |_, _| {
++ let fut = async move {
++ torrent
++ .reannounce()
++ .await
++ .expect("Unable to reannounce torrent");
++ };
++ glib::spawn_future_local(fut);
++ }
++ ));
+ actions.add_action(&manual_update_action);
+
+ // torrent.queue-up
+ let queue_up_action = SimpleAction::new("queue-up", None);
+- queue_up_action.connect_activate(clone!(@weak torrent => move |_, _| {
+- move_queue(false, torrent);
+- }));
++ queue_up_action.connect_activate(clone!(
++ #[weak]
++ torrent,
++ move |_, _| {
++ move_queue(false, torrent);
++ }
++ ));
+ actions.add_action(&queue_up_action);
+
+ // torrent.queue-down
+ let queue_down_action = SimpleAction::new("queue-down", None);
+- queue_down_action.connect_activate(clone!(@weak torrent => move |_, _| {
+- move_queue(true, torrent);
+- }));
++ queue_down_action.connect_activate(clone!(
++ #[weak]
++ torrent,
++ move |_, _| {
++ move_queue(true, torrent);
++ }
++ ));
+ actions.add_action(&queue_down_action);
+
+ // Disable queue actions for non queued torrents
+- torrent.connect_status_notify(
+- clone!(@weak torrent, @weak queue_up_action, @weak queue_down_action => move |_|{
++ torrent.connect_status_notify(clone!(
++ #[weak]
++ torrent,
++ #[weak]
++ queue_up_action,
++ #[weak]
++ queue_down_action,
++ move |_| {
+ update_queue_action(&torrent, &queue_up_action, &queue_down_action);
+- }),
+- );
++ }
++ ));
+ update_queue_action(torrent, &queue_up_action, &queue_down_action);
+
+ // torrent.show-dialog
+ let show_dialog_action = SimpleAction::new("show-dialog", None);
+- show_dialog_action.connect_activate(clone!(@weak torrent, @weak widget => move |_, _| {
+- let dialog = FrgTorrentDialog::new(&torrent);
+- dialog.present(Some(&widget));
+- }));
++ show_dialog_action.connect_activate(clone!(
++ #[weak]
++ torrent,
++ #[weak]
++ widget,
++ move |_, _| {
++ let dialog = FrgTorrentDialog::new(&torrent);
++ dialog.present(Some(&widget));
++ }
++ ));
+ actions.add_action(&show_dialog_action);
+
+ // torrent.open
+ let open_action = SimpleAction::new("open", None);
+- open_action.connect_activate(clone!(@weak torrent, @weak widget => move |_, _| {
+- let name = if let Some(top_level) = torrent.files().top_level() {
+- top_level.name()
+- } else {
+- warn!("Top level file is not available, falling back to torrent name for opening");
+- torrent.name()
+- };
+-
+- let path = Path::new(&torrent.download_dir()).join(name);
+- open(path, &widget.native().unwrap(), false, Some(widget));
+- }));
++ open_action.connect_activate(clone!(
++ #[weak]
++ torrent,
++ #[weak]
++ widget,
++ move |_, _| {
++ let name = if let Some(top_level) = torrent.files().top_level() {
++ top_level.name()
++ } else {
++ warn!("Top level file is not available, falling back to torrent name for opening");
++ torrent.name()
++ };
++
++ let path = Path::new(&torrent.download_dir()).join(name);
++ open(path, &widget.native().unwrap(), false, Some(widget));
++ }
++ ));
+ actions.add_action(&open_action);
+
+ // Only downloaded torrents can be opened
+- torrent.connect_downloaded_notify(clone!(@weak torrent, @weak open_action => move |_|{
+- update_torrent_open_action(&torrent, &open_action);
+- }));
++ torrent.connect_downloaded_notify(clone!(
++ #[weak]
++ torrent,
++ #[weak]
++ open_action,
++ move |_| {
++ update_torrent_open_action(&torrent, &open_action);
++ }
++ ));
+ update_torrent_open_action(torrent, &open_action);
+
+ // torrent.set-location
+ let set_location_action = SimpleAction::new("set-location", None);
+- set_location_action.connect_activate(clone!(@weak torrent, @weak widget => move |_, _| {
++ set_location_action.connect_activate(clone!(
++ #[weak]
++ torrent,
++ #[weak]
++ widget,
++ move |_, _| {
+ show_set_torrent_location(&torrent, &widget);
+ }
+ ));
+@@ -158,12 +234,18 @@ pub fn install_torrent_actions(torrent:
+ actions.add_action(&set_location_action);
+
+ // Disable filesystem actions when connected with a remote session
+- current_connection.connect_is_fragments_notify(
+- clone!(@weak torrent, @weak open_action, @weak set_location_action => move |c|{
++ current_connection.connect_is_fragments_notify(clone!(
++ #[weak]
++ torrent,
++ #[weak]
++ open_action,
++ #[weak]
++ set_location_action,
++ move |c| {
+ set_location_action.set_enabled(c.is_fragments());
+ update_torrent_open_action(&torrent, &open_action);
+- }),
+- );
++ }
++ ));
+ }
+
+ pub fn install_file_actions(file: &TrFile, widget: gtk::Widget) {
+@@ -174,44 +256,73 @@ pub fn install_file_actions(file: &TrFil
+
+ // file.open
+ let open_action = SimpleAction::new("open", None);
+- open_action.connect_activate(clone!(@weak file, @weak widget => move |_, _| {
+- let path = Path::new(&file.torrent().download_dir()).join(file.name());
+- open(path, &widget.native().unwrap(), false, Some(widget));
+- }));
++ open_action.connect_activate(clone!(
++ #[weak]
++ file,
++ #[weak]
++ widget,
++ move |_, _| {
++ let path = Path::new(&file.torrent().download_dir()).join(file.name());
++ open(path, &widget.native().unwrap(), false, Some(widget));
++ }
++ ));
+ actions.add_action(&open_action);
+
+ // file.open-containing-folder
+ let open_containing_action = SimpleAction::new("open-containing-folder", None);
+- open_containing_action.connect_activate(clone!(@weak file, @weak widget => move |_, _| {
+- let path = Path::new(&file.torrent().download_dir()).join(file.name());
+- open(path, &widget.native().unwrap(), true, Some(widget));
+- }));
++ open_containing_action.connect_activate(clone!(
++ #[weak]
++ file,
++ #[weak]
++ widget,
++ move |_, _| {
++ let path = Path::new(&file.torrent().download_dir()).join(file.name());
++ open(path, &widget.native().unwrap(), true, Some(widget));
++ }
++ ));
+ actions.add_action(&open_containing_action);
+
+ // Only downloaded files can be opened
+- file.connect_bytes_completed_notify(clone!(@weak file, @weak actions => move |_|{
+- update_file_open_actions(&file, &actions);
+- }));
++ file.connect_bytes_completed_notify(clone!(
++ #[weak]
++ file,
++ #[weak]
++ actions,
++ move |_| {
++ update_file_open_actions(&file, &actions);
++ }
++ ));
+ update_file_open_actions(file, &actions);
+
+ // Disable filesystem actions when connected with a remote session
+- current_connection.connect_is_fragments_notify(
+- clone!(@weak file, @weak open_action, @weak actions => move |c|{
++ current_connection.connect_is_fragments_notify(clone!(
++ #[weak]
++ file,
++ #[weak]
++ actions,
++ move |c| {
+ open_containing_action.set_enabled(c.is_fragments());
+ update_file_open_actions(&file, &actions);
+- }),
+- );
++ }
++ ));
+ }
+
+ fn move_queue(down: bool, torrent: TrTorrent) {
+- let fut = clone!(@weak torrent => async move {
+- let pos = if down{
+- torrent.download_queue_position() + 1
+- }else{
+- torrent.download_queue_position() - 1
+- };
+- torrent.set_download_queue_position(pos).await.expect("Unable to set download queue position");
+- });
++ let fut = clone!(
++ #[weak]
++ torrent,
++ async move {
++ let pos = if down {
++ torrent.download_queue_position() + 1
++ } else {
++ torrent.download_queue_position() - 1
++ };
++ torrent
++ .set_download_queue_position(pos)
++ .await
++ .expect("Unable to set download queue position");
++ }
++ );
+ glib::spawn_future_local(fut);
+ }
+
+@@ -282,22 +393,36 @@ fn show_remove_torrent_dialog(torrent: &
+ check_button.set_halign(gtk::Align::Center);
+ dialog.set_extra_child(Some(&check_button));
+
+- dialog.connect_response(None,
+- clone!(@strong dialog, @weak torrent, @weak parent, @weak check_button => move |_ , resp| {
+- if resp == "remove" {
+- let fut = async move {
+- torrent.remove(check_button.is_active()).await.expect("Unable to remove torrent");
+- };
++ dialog.connect_response(
++ None,
++ clone!(
++ #[strong]
++ dialog,
++ #[weak]
++ torrent,
++ #[weak]
++ parent,
++ #[weak]
++ check_button,
++ move |_, resp| {
++ if resp == "remove" {
++ let fut = async move {
++ torrent
++ .remove(check_button.is_active())
++ .await
++ .expect("Unable to remove torrent");
++ };
++
++ if let Ok(dialog) = parent.downcast::<adw::Dialog>() {
++ dialog.close();
++ }
+
+- if let Ok(dialog) = parent.downcast::<adw::Dialog>(){
+- dialog.close();
++ glib::spawn_future_local(fut);
+ }
+
+- glib::spawn_future_local(fut);
++ dialog.close();
+ }
+-
+- dialog.close();
+- }),
++ ),
+ );
+
+ dialog.present(Some(&parent));
+@@ -312,7 +437,7 @@ fn show_set_torrent_location(torrent: &T
+ dialog.set_accept_label(Some(&i18n("_Select")));
+
+ let root = parent.root().unwrap().downcast::<gtk::Window>().unwrap();
+- dialog.select_folder(Some(&root), gio::Cancellable::NONE, clone!(@weak torrent, @weak parent, @strong dialog => move |result| {
++ dialog.select_folder(Some(&root), gio::Cancellable::NONE, clone!(#[weak] torrent, #[weak] parent, #[strong(rename_to = _dialog)] dialog, move |result| {
+ match result {
+ Ok(folder) => {
+ debug!("Selected torrent directory: {:?}", folder.path());
+@@ -330,9 +455,9 @@ fn show_set_torrent_location(torrent: &T
+ dialog.present(Some(&parent));
+ dialog.connect_response(
+ None,
+- clone!(@strong dialog, @strong torrent, @strong folder => move |_ , resp| {
++ clone!(#[strong(rename_to = _dialog)] dialog, #[strong] torrent, #[strong] folder, move |_ , resp| {
+ let resp = resp.to_string();
+- let fut = clone!(@strong resp, @strong torrent, @strong folder => async move {
++ let fut = clone!(#[strong] resp, #[strong] torrent, #[strong] folder, async move {
+ let _ = torrent.set_location(folder, resp == "move-data").await;
+ });
+
+@@ -364,28 +489,51 @@ fn open(
+ open_containing_folder: bool,
+ widget: Option<gtk::Widget>,
+ ) {
+- let fut = clone!(@strong path, @weak native, @strong open_containing_folder, @strong widget => async move {
+- debug!("Opening: {:?} (containing folder: {})", path, open_containing_folder);
+- match File::open(&path){
+- Ok(file) => {
+- let identifier = WindowIdentifier::from_native(&native).await;
+-
+- let res = if open_containing_folder || path.is_dir() {
+- OpenDirectoryRequest::default().identifier(identifier).send(&file.as_fd()).await
+- } else {
+- OpenFileRequest::default().identifier(identifier).ask(true).send_file(&file.as_fd()).await
+- };
++ let fut = clone!(
++ #[strong]
++ path,
++ #[weak]
++ native,
++ #[strong]
++ open_containing_folder,
++ #[strong]
++ widget,
++ async move {
++ debug!(
++ "Opening: {:?} (containing folder: {})",
++ path, open_containing_folder
++ );
++ match File::open(&path) {
++ Ok(file) => {
++ let identifier = WindowIdentifier::from_native(&native).await;
++
++ let res = if open_containing_folder || path.is_dir() {
++ OpenDirectoryRequest::default()
++ .identifier(identifier)
++ .send(&file.as_fd())
++ .await
++ } else {
++ OpenFileRequest::default()
++ .identifier(identifier)
++ .ask(true)
++ .send_file(&file.as_fd())
++ .await
++ };
+
+- if let Err(err) = res {
+- warn!("Could not open folder / file: {}", err.to_string());
+- utils::inapp_notification(&i18n("Unable to open file / folder"), widget.as_ref());
++ if let Err(err) = res {
++ warn!("Could not open folder / file: {}", err.to_string());
++ utils::inapp_notification(
++ &i18n("Unable to open file / folder"),
++ widget.as_ref(),
++ );
++ }
++ }
++ Err(err) => {
++ warn!("Could not open file: {}", err.to_string());
++ utils::inapp_notification(&i18n("Could not open file"), widget.as_ref());
+ }
+- },
+- Err(err) => {
+- warn!("Could not open file: {}", err.to_string());
+- utils::inapp_notification(&i18n("Could not open file"), widget.as_ref());
+ }
+ }
+- });
++ );
+ glib::spawn_future_local(fut);
+ }
+Index: fragments-3.0.1/src/backend/connection.rs
+===================================================================
+--- fragments-3.0.1.orig/src/backend/connection.rs
++++ fragments-3.0.1/src/backend/connection.rs
+@@ -1,5 +1,5 @@
+ // Fragments - connection.rs
+-// Copyright (C) 2022-2023 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+Index: fragments-3.0.1/src/backend/connection_manager.rs
+===================================================================
+--- fragments-3.0.1.orig/src/backend/connection_manager.rs
++++ fragments-3.0.1/src/backend/connection_manager.rs
+@@ -1,5 +1,5 @@
+ // Fragments - connection_manager.rs
+-// Copyright (C) 2022-2024 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -112,15 +112,22 @@ mod imp {
+ self.parent_constructed();
+
+ let nm = gio::NetworkMonitor::default();
+- nm.connect_network_metered_notify(clone!(@weak self as this => move |_| {
+- this.check_for_stop_reason();
+- }));
++ nm.connect_network_metered_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.check_for_stop_reason();
++ }
++ ));
+
+ self.read_connections();
+- self.connections
+- .connect_items_changed(clone!(@weak self as this => move |_, _, _, _|{
++ self.connections.connect_items_changed(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, _, _, _| {
+ this.write_connections();
+- }));
++ }
++ ));
+ }
+
+ fn signals() -> &'static [Signal] {
+@@ -214,22 +221,38 @@ mod imp {
+ }
+
+ info!("Reason for stopping daemon: {:?}", reason);
+- let fut = clone!(@weak self as this, @strong reason => async move {
+- let res = this.stop_daemon(&reason).await;
+- if let Err(e) = res {
+- utils::inapp_notification(&i18n("Unable to stop transmission-daemon"), gtk::Widget::NONE);
+- error!("Unable to stop transmission-daemon: {}", e.to_string());
++ let fut = clone!(
++ #[weak(rename_to = this)]
++ self,
++ #[strong]
++ reason,
++ async move {
++ let res = this.stop_daemon(&reason).await;
++ if let Err(e) = res {
++ utils::inapp_notification(
++ &i18n("Unable to stop transmission-daemon"),
++ gtk::Widget::NONE,
++ );
++ error!("Unable to stop transmission-daemon: {}", e.to_string());
++ }
+ }
+- });
++ );
+ glib::spawn_future_local(fut);
+ } else if stop_reason.is_none() && is_fragments {
+- let fut = clone!(@weak self as this => async move {
+- let res = this.start_daemon().await;
+- if let Err(e) = res {
+- utils::inapp_notification(&i18n("Unable to start transmission-daemon"), gtk::Widget::NONE);
+- error!("Unable to start transmission-daemon: {}", e.to_string());
++ let fut = clone!(
++ #[weak(rename_to = this)]
++ self,
++ async move {
++ let res = this.start_daemon().await;
++ if let Err(e) = res {
++ utils::inapp_notification(
++ &i18n("Unable to start transmission-daemon"),
++ gtk::Widget::NONE,
++ );
++ error!("Unable to start transmission-daemon: {}", e.to_string());
++ }
+ }
+- });
++ );
+ glib::spawn_future_local(fut);
+ }
+ }
+Index: fragments-3.0.1/src/backend/daemon.rs
+===================================================================
+--- fragments-3.0.1.orig/src/backend/daemon.rs
++++ fragments-3.0.1/src/backend/daemon.rs
+@@ -1,3 +1,19 @@
++// Fragments - daemon.rs
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
++//
++// This program is free software: you can redistribute it and/or modify
++// it under the terms of the GNU General Public License as published by
++// the Free Software Foundation, either version 3 of the License, or
++// (at your option) any later version.
++//
++// This program is distributed in the hope that it will be useful,
++// but WITHOUT ANY WARRANTY; without even the implied warranty of
++// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++// GNU General Public License for more details.
++//
++// You should have received a copy of the GNU General Public License
++// along with this program. If not, see <https://www.gnu.org/licenses/>.
++
+ use std::cell::RefCell;
+ use std::fs;
+ use std::path::Path;
+Index: fragments-3.0.1/src/ui/connection_popover.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/connection_popover.rs
++++ fragments-3.0.1/src/ui/connection_popover.rs
+@@ -1,5 +1,5 @@
+ // Fragments - connection_popover.rs
+-// Copyright (C) 2022-2023 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -53,14 +53,20 @@ mod imp {
+ self.parent_constructed();
+ let cm = FrgConnectionManager::default();
+
+- self.listbox
+- .connect_row_activated(clone!(@weak self as this => move |_, row|{
++ self.listbox.connect_row_activated(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, row| {
+ let row = row.downcast_ref::<FrgConnectionRow>().unwrap();
+ let app = FrgApplication::default();
+
+- app.activate_action("set-connection", Some(&row.connection().uuid().to_variant()));
++ app.activate_action(
++ "set-connection",
++ Some(&row.connection().uuid().to_variant()),
++ );
+ this.obj().popdown();
+- }));
++ }
++ ));
+
+ self.listbox
+ .bind_model(Some(&cm.connections()), |connection| {
+@@ -68,21 +74,31 @@ mod imp {
+ .upcast()
+ });
+
+- cm.connect_current_connection_notify(clone!(@weak self as this => move |_|{
+- this.update_ui();
+- }));
++ cm.connect_current_connection_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_ui();
++ }
++ ));
+
+- cm.client()
+- .connect_is_busy_notify(clone!(@weak self as this => move |client|{
++ cm.client().connect_is_busy_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |client| {
+ // Don't allow changing the active connection
+ // when the client is still busy establishing a connection
+ this.obj().set_sensitive(!client.is_busy());
+- }));
++ }
++ ));
+
+- cm.connections()
+- .connect_items_changed(clone!(@weak self as this => move |_,_,_,_|{
++ cm.connections().connect_items_changed(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, _, _, _| {
+ this.update_ui();
+- }));
++ }
++ ));
+
+ self.update_ui();
+ }
+@@ -111,5 +127,6 @@ mod imp {
+ glib::wrapper! {
+ pub struct FrgConnectionPopover(
+ ObjectSubclass<imp::FrgConnectionPopover>)
+- @extends gtk::Widget, gtk::Popover;
++ @extends gtk::Widget, gtk::Popover,
++ @implements gtk::Native, gtk::ShortcutManager, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+Index: fragments-3.0.1/src/ui/connection_row.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/connection_row.rs
++++ fragments-3.0.1/src/ui/connection_row.rs
+@@ -1,5 +1,5 @@
+ // Fragments - connection_row.rs
+-// Copyright (C) 2022-2023 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -126,7 +126,8 @@ mod imp {
+ glib::wrapper! {
+ pub struct FrgConnectionRow(
+ ObjectSubclass<imp::FrgConnectionRow>)
+- @extends gtk::Widget, gtk::ListBoxRow;
++ @extends gtk::Widget, gtk::ListBoxRow,
++ @implements gtk::Actionable, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ impl FrgConnectionRow {
+Index: fragments-3.0.1/src/ui/drag_overlay.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/drag_overlay.rs
++++ fragments-3.0.1/src/ui/drag_overlay.rs
+@@ -1,5 +1,5 @@
+ // Fragments - drag_overlay.rs
+-// Copyright (C) 2023 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -57,12 +57,14 @@ mod imp {
+ self.revealer.set_reveal_child(false);
+
+ self.drop_target.set_actions(gtk::gdk::DragAction::COPY);
+- self.drop_target.connect_current_drop_notify(
+- glib::clone!(@weak self.revealer as revealer => move |target| {
++ self.drop_target.connect_current_drop_notify(glib::clone!(
++ #[weak(rename_to = revealer)]
++ self.revealer,
++ move |target| {
+ let reveal = target.current_drop().is_some();
+ revealer.set_reveal_child(reveal);
+- }),
+- );
++ }
++ ));
+
+ let bin = adw::Bin::new();
+ bin.set_can_target(false);
+@@ -90,5 +92,6 @@ mod imp {
+ glib::wrapper! {
+ pub struct FrgDragOverlay(
+ ObjectSubclass<imp::FrgDragOverlay>)
+- @extends gtk::Widget, adw::Bin;
++ @extends gtk::Widget, adw::Bin,
++ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+Index: fragments-3.0.1/src/ui/file_page.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/file_page.rs
++++ fragments-3.0.1/src/ui/file_page.rs
+@@ -1,5 +1,5 @@
+ // Fragments - file_page.rs
+-// Copyright (C) 2023 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -66,7 +66,7 @@ mod imp {
+
+ self.status_header.set_title(file.title());
+
+- let mimetype = gio::content_type_guess(Some(&file.name()), &[])
++ let mimetype = gio::content_type_guess(Some(&file.name()), None)
+ .0
+ .to_string();
+ self.status_header.set_mimetype(mimetype);
+@@ -106,7 +106,8 @@ mod imp {
+
+ glib::wrapper! {
+ pub struct FrgFilePage(ObjectSubclass<imp::FrgFilePage>)
+- @extends gtk::Widget, adw::NavigationPage;
++ @extends gtk::Widget, adw::NavigationPage,
++ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ impl FrgFilePage {
+Index: fragments-3.0.1/src/ui/file_row.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/file_row.rs
++++ fragments-3.0.1/src/ui/file_row.rs
+@@ -1,5 +1,5 @@
+ // Fragments - file_row.rs
+-// Copyright (C) 2022-2023 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -106,7 +106,7 @@ mod imp {
+ self.mimetype_image.set_icon_name(Some("folder-symbolic"));
+ self.folder_arrow_image.set_visible(true);
+ } else {
+- let mimetype = gio::content_type_guess(Some(&file.name()), &[])
++ let mimetype = gio::content_type_guess(Some(&file.name()), None)
+ .0
+ .to_string();
+ utils::set_mimetype_image(&mimetype, &self.mimetype_image.get())
+@@ -142,19 +142,27 @@ mod imp {
+ // Progress & Subtitle
+ let id = file.connect_notify_local(
+ Some("bytes-completed"),
+- clone!(@weak self as this => move|_,_|{
+- this.update_progressbar();
+- this.update_subtitle();
+- }),
++ clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, _| {
++ this.update_progressbar();
++ this.update_subtitle();
++ }
++ ),
+ );
+ self.signal_handlers.borrow_mut().push(id);
+
+ let id = file.connect_notify_local(
+ Some("wanted"),
+- clone!(@weak self as this => move|_,_|{
+- this.update_progressbar();
+- this.update_subtitle();
+- }),
++ clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, _| {
++ this.update_progressbar();
++ this.update_subtitle();
++ }
++ ),
+ );
+ self.signal_handlers.borrow_mut().push(id);
+
+@@ -213,7 +221,8 @@ mod imp {
+
+ glib::wrapper! {
+ pub struct FrgFileRow(ObjectSubclass<imp::FrgFileRow>)
+- @extends gtk::Widget, adw::Bin;
++ @extends gtk::Widget, adw::Bin,
++ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ impl FrgFileRow {
+Index: fragments-3.0.1/src/ui/folder_page.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/folder_page.rs
++++ fragments-3.0.1/src/ui/folder_page.rs
+@@ -1,5 +1,5 @@
+ // Fragments - folder_page.rs
+-// Copyright (C) 2023 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -82,7 +82,8 @@ mod imp {
+
+ glib::wrapper! {
+ pub struct FrgFolderPage(ObjectSubclass<imp::FrgFolderPage>)
+- @extends gtk::Widget, adw::NavigationPage;
++ @extends gtk::Widget, adw::NavigationPage,
++ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ impl FrgFolderPage {
+Index: fragments-3.0.1/src/ui/folder_page_contents.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/folder_page_contents.rs
++++ fragments-3.0.1/src/ui/folder_page_contents.rs
+@@ -1,5 +1,5 @@
+ // Fragments - folder_page_contents.rs
+-// Copyright (C) 2023 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -81,13 +81,21 @@ mod imp {
+ .set_model(Some(&self.obj().file().related()));
+
+ // Needed, since listview needs a moment to populate the list
+- glib::idle_add_local_once(clone!(@weak self as this => move|| {
+- this.scrolled_window.vadjustment().set_value(*this.scroll_pos.get().unwrap());
+- }));
++ glib::idle_add_local_once(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move || {
++ this.scrolled_window
++ .vadjustment()
++ .set_value(*this.scroll_pos.get().unwrap());
++ }
++ ));
+
+ // Search
+- self.searchbar.connect_search_mode_enabled_notify(
+- clone!(@weak self as this => move |sb| {
++ self.searchbar.connect_search_mode_enabled_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |sb| {
+ let file = this.obj().file();
+
+ let model: gio::ListModel = if sb.is_search_mode() {
+@@ -99,8 +107,8 @@ mod imp {
+ };
+
+ this.sort_model.set_model(Some(&model));
+- }),
+- );
++ }
++ ));
+ self.searchentry
+ .bind_property("text", &self.filter.get(), "search")
+ .build();
+@@ -113,16 +121,24 @@ mod imp {
+
+ // view.select-all
+ action = SimpleAction::new("select-all", None);
+- action.connect_activate(clone!(@weak self as this => move |_, _| {
+- this.obj().file().set_wanted(true);
+- }));
++ action.connect_activate(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, _| {
++ this.obj().file().set_wanted(true);
++ }
++ ));
+ self.view_actions.add_action(&action);
+
+ // view.deselect-all
+ action = SimpleAction::new("deselect-all", None);
+- action.connect_activate(clone!(@weak self as this => move |_, _| {
+- this.obj().file().set_wanted(false);
+- }));
++ action.connect_activate(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, _| {
++ this.obj().file().set_wanted(false);
++ }
++ ));
+ self.view_actions.add_action(&action);
+ }
+ }
+@@ -157,7 +173,8 @@ mod imp {
+
+ glib::wrapper! {
+ pub struct FrgFolderPageContents(ObjectSubclass<imp::FrgFolderPageContents>)
+- @extends gtk::Widget, adw::Bin;
++ @extends gtk::Widget, adw::Bin,
++ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ impl FrgFolderPageContents {
+Index: fragments-3.0.1/src/ui/preferences_dialog.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/preferences_dialog.rs
++++ fragments-3.0.1/src/ui/preferences_dialog.rs
+@@ -1,5 +1,5 @@
+ // Fragments - preferences_dialog.rs
+-// Copyright (C) 2022-2024 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -118,11 +118,14 @@ mod imp {
+ let session = client.session();
+
+ // We show/hide few widgets depending on the connection type (remote/local)
+- self.connection_manager.connect_current_connection_notify(
+- clone!(@weak self as this => move |_|{
+- this.update_remote_buttons();
+- }),
+- );
++ self.connection_manager
++ .connect_current_connection_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_remote_buttons();
++ }
++ ));
+
+ // General page
+ //
+@@ -155,23 +158,39 @@ mod imp {
+ &*self.remote_access_switch,
+ "active",
+ );
+- self.remote_access_switch.connect_state_set(clone!(@weak self as this => @default-panic, move |switch, _| {
+- settings_manager::set_boolean(Key::RemoteAccess, switch.is_active());
+- switch.set_sensitive(false);
+-
+- let fut = clone!(@weak switch, @weak this => async move {
+- if let Err(err) = FrgConnectionManager::default().reconnect().await{
+- utils::inapp_notification(&i18n("Unable to restart Transmission daemon"), Some(this.obj().upcast_ref::<gtk::Widget>()));
+- warn!("Unable to restart Transmission daemon: {}", err.to_string());
+- }
++ self.remote_access_switch.connect_state_set(clone!(
++ #[weak(rename_to = this)]
++ self,
++ #[upgrade_or_panic]
++ move |switch, _| {
++ settings_manager::set_boolean(Key::RemoteAccess, switch.is_active());
++ switch.set_sensitive(false);
++
++ let fut = clone!(
++ #[weak]
++ switch,
++ #[weak]
++ this,
++ async move {
++ if let Err(err) = FrgConnectionManager::default().reconnect().await {
++ utils::inapp_notification(
++ &i18n("Unable to restart Transmission daemon"),
++ Some(this.obj().upcast_ref::<gtk::Widget>()),
++ );
++ warn!("Unable to restart Transmission daemon: {}", err.to_string());
++ }
+
+- switch.set_sensitive(true);
+- });
+- glib::spawn_future_local(fut);
+- glib::Propagation::Proceed
+- }));
+- self.open_webinterface_row
+- .connect_activated(clone!(@weak self as this => move |_| {
++ switch.set_sensitive(true);
++ }
++ );
++ glib::spawn_future_local(fut);
++ glib::Propagation::Proceed
++ }
++ ));
++ self.open_webinterface_row.connect_activated(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
+ let fut = async move {
+ let uri = url::Url::parse("http://127.0.0.1:9091/").unwrap();
+ let native = this.obj().native().unwrap();
+@@ -182,21 +201,33 @@ mod imp {
+ .await
+ {
+ debug!("Unable to open webinterface: {:?}", err);
+- utils::inapp_notification("Unable to open webinterface", Some(this.obj().upcast_ref::<gtk::Widget>()));
++ utils::inapp_notification(
++ "Unable to open webinterface",
++ Some(this.obj().upcast_ref::<gtk::Widget>()),
++ );
+ }
+ };
+ glib::spawn_future_local(fut);
+- }));
++ }
++ ));
+
+ // Downloading page
+ //
+- client.connect_is_connected_notify(clone!(@weak self as this => move |_|{
+- this.update_paths();
+- }));
+-
+- session.connect_download_dir_notify(glib::clone!(@weak self as this => move |_| {
+- this.update_paths();
+- }));
++ client.connect_is_connected_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_paths();
++ }
++ ));
++
++ session.connect_download_dir_notify(glib::clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_paths();
++ }
++ ));
+
+ self.download_dir_button.connect_clicked(move |btn| {
+ let parent = btn.root().unwrap().downcast::<gtk::Window>().unwrap();
+@@ -212,15 +243,17 @@ mod imp {
+ .flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL)
+ .build();
+
+- session.connect_incomplete_dir_enabled_notify(
+- glib::clone!(@weak self as this => move |_| {
+- FrgConnectionManager::default().check_directories();
+- }),
+- );
+-
+- session.connect_incomplete_dir_notify(glib::clone!(@weak self as this => move |_| {
+- this.update_paths();
+- }));
++ session.connect_incomplete_dir_enabled_notify(glib::clone!(move |_| {
++ FrgConnectionManager::default().check_directories()
++ },));
++
++ session.connect_incomplete_dir_notify(glib::clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_paths();
++ }
++ ));
+
+ self.incomplete_dir_button.connect_clicked(move |btn| {
+ let parent = btn.root().unwrap().downcast::<gtk::Window>().unwrap();
+@@ -378,7 +411,8 @@ mod imp {
+ glib::wrapper! {
+ pub struct FrgPreferencesDialog(
+ ObjectSubclass<imp::FrgPreferencesDialog>)
+- @extends gtk::Widget, adw::Dialog, adw::PreferencesDialog;
++ @extends gtk::Widget, adw::Dialog, adw::PreferencesDialog,
++ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ impl Default for FrgPreferencesDialog {
+Index: fragments-3.0.1/src/ui/stats_dialog.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/stats_dialog.rs
++++ fragments-3.0.1/src/ui/stats_dialog.rs
+@@ -1,5 +1,5 @@
+ // Fragments - stats_dialog.rs
+-// Copyright (C) 2022-2023 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -91,11 +91,13 @@ mod imp {
+ fn constructed(&self) {
+ self.parent_constructed();
+
+- FrgConnectionManager::default().connect_current_connection_notify(
+- clone!(@weak self as this => move |_|{
++ FrgConnectionManager::default().connect_current_connection_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
+ this.update_remote_notice();
+- }),
+- );
++ }
++ ));
+
+ self.bind_properties();
+ self.update_format_strings();
+@@ -179,41 +181,77 @@ mod imp {
+ .flags(BindingFlags::SYNC_CREATE)
+ .build();
+
+- stats.connect_download_speed_notify(clone!(@weak self as this => move |_|{
+- this.update_format_strings();
+- }));
+-
+- stats.connect_upload_speed_notify(clone!(@weak self as this => move |_|{
+- this.update_format_strings();
+- }));
+-
+- current.connect_seconds_active_notify(clone!(@weak self as this => move |_|{
+- this.update_format_strings();
+- }));
+-
+- current.connect_downloaded_bytes_notify(clone!(@weak self as this => move |_|{
+- this.update_format_strings();
+- }));
+-
+- current.connect_uploaded_bytes_notify(clone!(@weak self as this => move |_|{
+- this.update_format_strings();
+- }));
+-
+- cumulative.connect_seconds_active_notify(clone!(@weak self as this => move |_|{
+- this.update_format_strings();
+- }));
+-
+- cumulative.connect_downloaded_bytes_notify(clone!(@weak self as this => move |_|{
+- this.update_format_strings();
+- }));
+-
+- cumulative.connect_uploaded_bytes_notify(clone!(@weak self as this => move |_|{
+- this.update_format_strings();
+- }));
+-
+- cumulative.connect_session_count_notify(clone!(@weak self as this => move |_|{
+- this.update_format_strings();
+- }));
++ stats.connect_download_speed_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_format_strings();
++ }
++ ));
++
++ stats.connect_upload_speed_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_format_strings();
++ }
++ ));
++
++ current.connect_seconds_active_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_format_strings();
++ }
++ ));
++
++ current.connect_downloaded_bytes_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_format_strings();
++ }
++ ));
++
++ current.connect_uploaded_bytes_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_format_strings();
++ }
++ ));
++
++ cumulative.connect_seconds_active_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_format_strings();
++ }
++ ));
++
++ cumulative.connect_downloaded_bytes_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_format_strings();
++ }
++ ));
++
++ cumulative.connect_uploaded_bytes_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_format_strings();
++ }
++ ));
++
++ cumulative.connect_session_count_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_format_strings();
++ }
++ ));
+ }
+
+ fn update_format_strings(&self) {
+@@ -272,7 +310,8 @@ mod imp {
+
+ glib::wrapper! {
+ pub struct FrgStatsDialog(ObjectSubclass<imp::FrgStatsDialog>)
+- @extends gtk::Widget, adw::Dialog;
++ @extends gtk::Widget, adw::Dialog,
++ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ impl FrgStatsDialog {
+Index: fragments-3.0.1/src/ui/status_header.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/status_header.rs
++++ fragments-3.0.1/src/ui/status_header.rs
+@@ -1,5 +1,5 @@
+ // Fragments - status_header.rs
+-// Copyright (C) s2023 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -120,7 +120,8 @@ mod imp {
+
+ glib::wrapper! {
+ pub struct FrgStatusHeader(ObjectSubclass<imp::FrgStatusHeader>)
+- @extends gtk::Widget, adw::Bin;
++ @extends gtk::Widget, adw::Bin,
++ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ impl FrgStatusHeader {
+Index: fragments-3.0.1/src/ui/torrent_dialog.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/torrent_dialog.rs
++++ fragments-3.0.1/src/ui/torrent_dialog.rs
+@@ -1,5 +1,5 @@
+ // Fragments - torrent_dialog.rs
+-// Copyright (C) 2022-2024 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -170,16 +170,24 @@ mod imp {
+ if torrent.files().is_ready() {
+ self.show_overview_files();
+ }
+- torrent
+- .files()
+- .connect_is_ready_notify(clone!(@weak self as this => move|_| {
++ torrent.files().connect_is_ready_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
+ this.show_overview_files();
+- }));
++ }
++ ));
+
+ // Files listbox row activation
+- self.file_listbox
+- .connect_row_activated(clone!(@weak self as this => move |_, row|{
+- let row = row.downcast_ref::<adw::PreferencesRow>().unwrap().child().unwrap();
++ self.file_listbox.connect_row_activated(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, row| {
++ let row = row
++ .downcast_ref::<adw::PreferencesRow>()
++ .unwrap()
++ .child()
++ .unwrap();
+ let file_row = row.downcast_ref::<FrgFileRow>().unwrap();
+ let file = file_row.file();
+
+@@ -190,7 +198,8 @@ mod imp {
+ this.show_file(&file);
+ }
+ }
+- }));
++ }
++ ));
+
+ // Setup view actions
+ obj.insert_action_group("view", Some(&self.view_actions));
+@@ -202,16 +211,18 @@ mod imp {
+ VariantTy::STRING.into(),
+ &"name".to_variant(),
+ );
+- action.connect_change_state(
+- clone!(@weak self.files_sorter as files_sorter => move |action, state|{
++ action.connect_change_state(clone!(
++ #[weak(rename_to = files_sorter)]
++ self.files_sorter,
++ move |action, state| {
+ if let Some(state) = state {
+ action.set_state(state);
+
+ let sorter: FrgFileSorting = state.str().unwrap().into();
+ files_sorter.set_sorting(sorter);
+ }
+- }),
+- );
++ }
++ ));
+ self.view_actions.add_action(&action);
+
+ // view.order
+@@ -220,73 +231,92 @@ mod imp {
+ VariantTy::STRING.into(),
+ &"ascending".to_variant(),
+ );
+- action.connect_change_state(
+- clone!(@weak self.files_sorter as files_sorter => move |action, state|{
++ action.connect_change_state(clone!(
++ #[weak(rename_to = files_sorter)]
++ self.files_sorter,
++ move |action, state| {
+ if let Some(state) = state {
+ action.set_state(state);
+
+ let descending = state.str().unwrap() == "descending";
+ files_sorter.set_descending(descending);
+ }
+- }),
+- );
++ }
++ ));
+ self.view_actions.add_action(&action);
+
+ // view.folder-before-files
+ action = SimpleAction::new_stateful("folder-before-files", None, &true.to_variant());
+- action.connect_change_state(
+- clone!(@weak self.files_sorter as files_sorter => move |action, state|{
++ action.connect_change_state(clone!(
++ #[weak(rename_to = files_sorter)]
++ self.files_sorter,
++ move |action, state| {
+ if let Some(state) = state {
+ action.set_state(state);
+
+ let value = state.get::<bool>().unwrap();
+ files_sorter.set_folders_before_files(value);
+ }
+- }),
+- );
++ }
++ ));
+ self.view_actions.add_action(&action);
+
+ // view.show-all
+ action = SimpleAction::new("show-all", None);
+- action.connect_activate(clone!(@weak self as this => move |_, _|{
+- if let Some(top_level) = this.obj().torrent().files().top_level(){
+- this.show_folder(&top_level);
+- } else {
+- warn!("Unable to show contents, no top level file available");
++ action.connect_activate(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, _| {
++ if let Some(top_level) = this.obj().torrent().files().top_level() {
++ this.show_folder(&top_level);
++ } else {
++ warn!("Unable to show contents, no top level file available");
++ }
+ }
+-
+- }));
++ ));
+ self.view_actions.add_action(&action);
+
+ // view.show-folder
+ action = SimpleAction::new("show-folder", VariantTy::STRING.into());
+- action.connect_activate(clone!(@weak self as this => move |_, param|{
+- if let Some(param) = param {
+- let name = param.get::<String>().unwrap();
+- let folder = this.obj().torrent().files().file_by_name(&name).unwrap();
+- this.show_folder(&folder);
++ action.connect_activate(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, param| {
++ if let Some(param) = param {
++ let name = param.get::<String>().unwrap();
++ let folder = this.obj().torrent().files().file_by_name(&name).unwrap();
++ this.show_folder(&folder);
++ }
+ }
+- }));
++ ));
+ self.view_actions.add_action(&action);
+
+ // view.show-file
+ action = SimpleAction::new("show-file", VariantTy::STRING.into());
+- action.connect_activate(clone!(@weak self as this => move |_, param|{
+- if let Some(param) = param {
+- let name = param.get::<String>().unwrap();
+- let file = this.obj().torrent().files().file_by_name(&name).unwrap();
+- this.show_file(&file);
++ action.connect_activate(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, param| {
++ if let Some(param) = param {
++ let name = param.get::<String>().unwrap();
++ let file = this.obj().torrent().files().file_by_name(&name).unwrap();
++ this.show_file(&file);
++ }
+ }
+- }));
++ ));
+ self.view_actions.add_action(&action);
+
+ // Update dialog on torrent changes
+ torrent.connect_notify_local(
+ None,
+- clone!(@weak self as this => move |_, _| {
+- this.update_labels();
+- this.update_widgets();
+- }),
++ clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, _| {
++ this.update_labels();
++ this.update_widgets();
++ }
++ ),
+ );
+
+ self.update_labels();
+@@ -463,7 +493,8 @@ mod imp {
+
+ glib::wrapper! {
+ pub struct FrgTorrentDialog(ObjectSubclass<imp::FrgTorrentDialog>)
+- @extends gtk::Widget, adw::Dialog;
++ @extends gtk::Widget, adw::Dialog,
++ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ impl FrgTorrentDialog {
+Index: fragments-3.0.1/src/ui/torrent_group.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/torrent_group.rs
++++ fragments-3.0.1/src/ui/torrent_group.rs
+@@ -1,5 +1,5 @@
+ // Fragments - torrent_group.rs
+-// Copyright (C) 2022-2023 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -60,7 +60,8 @@ mod imp {
+ glib::wrapper! {
+ pub struct FrgTorrentGroup(
+ ObjectSubclass<imp::FrgTorrentGroup>)
+- @extends gtk::Widget, adw::PreferencesGroup;
++ @extends gtk::Widget, adw::PreferencesGroup,
++ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ impl FrgTorrentGroup {
+@@ -81,8 +82,10 @@ impl FrgTorrentGroup {
+ row.activate_action("torrent.show-dialog", None).unwrap();
+ });
+
+- model.connect_items_changed(
+- glib::clone!(@weak self as this, @weak listbox => move |_, _, _, _| {
++ model.connect_items_changed(glib::clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, _, _, _| {
+ let imp = this.imp();
+
+ // Hide this group if all models are empty
+@@ -95,14 +98,14 @@ impl FrgTorrentGroup {
+ this.set_visible(!is_empty);
+
+ // Hide empty listboxes
+- let mut child = imp.listbox_box.first_child ();
++ let mut child = imp.listbox_box.first_child();
+ while let Some(widget) = child {
+ let listbox = widget.downcast_ref::<gtk::ListBox>().unwrap();
+ listbox.set_visible(listbox.row_at_index(0).is_some());
+ child = listbox.next_sibling();
+ }
+- }),
+- );
++ }
++ ));
+
+ imp.models.borrow_mut().insert(0, model);
+ }
+Index: fragments-3.0.1/src/ui/torrent_page.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/torrent_page.rs
++++ fragments-3.0.1/src/ui/torrent_page.rs
+@@ -1,5 +1,5 @@
+ // Fragments - torrent_page.rs
+-// Copyright (C) 2022-2024 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -110,11 +110,19 @@ mod imp {
+ model.connect_local(
+ "status-changed",
+ false,
+- clone!(@weak group_filter, @weak sorter => @default-return None, move |_| {
+- group_filter.changed(gtk::FilterChange::Different);
+- sorter.changed(gtk::SorterChange::Different);
+- None
+- }),
++ clone!(
++ #[weak]
++ group_filter,
++ #[weak]
++ sorter,
++ #[upgrade_or]
++ None,
++ move |_| {
++ group_filter.changed(gtk::FilterChange::Different);
++ sorter.changed(gtk::SorterChange::Different);
++ None
++ }
++ ),
+ );
+
+ let group_model = FilterListModel::new(Some(model), Some(group_filter));
+@@ -128,7 +136,8 @@ mod imp {
+ glib::wrapper! {
+ pub struct FrgTorrentPage(
+ ObjectSubclass<imp::FrgTorrentPage>)
+- @extends gtk::Widget, adw::PreferencesPage;
++ @extends gtk::Widget, adw::PreferencesPage,
++ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ impl FrgTorrentPage {
+Index: fragments-3.0.1/src/ui/window.rs
+===================================================================
+--- fragments-3.0.1.orig/src/ui/window.rs
++++ fragments-3.0.1/src/ui/window.rs
+@@ -101,26 +101,33 @@ mod imp {
+ self.search_bar.connect_entry(&self.search_entry.get());
+
+ // Recolor headerbar purple for remote connections
+- cm.connect_current_connection_notify(
+- clone!(@weak self as this => move |manager|{
+- if !manager.current_connection().is_fragments(){
+- let subtitle = i18n_f("Remote control \"{}\"", &[&manager.current_connection().title()]);
++ cm.connect_current_connection_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |manager| {
++ if !manager.current_connection().is_fragments() {
++ let subtitle = i18n_f(
++ "Remote control \"{}\"",
++ &[&manager.current_connection().title()],
++ );
+ this.window_title.set_subtitle(&subtitle);
+ this.obj().add_css_class("remote");
+- }else{
++ } else {
+ this.window_title.set_subtitle("");
+ this.obj().remove_css_class("remote");
+ }
+- }),
+- );
++ }
++ ));
+
+ // Show connection button if we have more than the standard local connection
+- cm.connections().connect_items_changed(
+- clone!(@weak self as this => move |model,_,_,_|{
++ cm.connections().connect_items_changed(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |model, _, _, _| {
+ let show = model.n_items() > 1;
+ this.connection_menu.set_visible(show);
+- }),
+- );
++ }
++ ));
+
+ // Check clipboard content for magnet links
+ obj.connect_is_active_notify(move |window| {
+@@ -128,73 +135,108 @@ mod imp {
+ let display = gdk::Display::default().unwrap();
+ let clipboard = display.primary_clipboard();
+
+- let fut = clone!(@weak window, @weak clipboard => async move {
+- let content = clipboard.read_text_future().await;
+- let mut clear_clipboard = false;
+-
+- if let Ok(Some(text)) = content {
+- // Check for magnet link
+- if let Ok(magnet_link) = MagnetURI::from_str(&text) {
+- let fallback = String::new();
+- let info_hash = magnet_link.info_hash().unwrap_or(&fallback);
+- let cm = FrgConnectionManager::default();
+-
+- // Transmission lowercases info hashes internally
+- if cm.client().torrents().torrent_by_hash(info_hash.to_lowercase()).is_none() {
+- debug!("Detected new magnet link: {}", &text);
+- window.imp().magnet_link_notification(&text, magnet_link.name());
+- clear_clipboard = true;
+- } else {
+- debug!("Ignore magnet link, torrent is already added.");
++ let fut = clone!(
++ #[weak]
++ window,
++ #[weak]
++ clipboard,
++ async move {
++ let content = clipboard.read_text_future().await;
++ let mut clear_clipboard = false;
++
++ if let Ok(Some(text)) = content {
++ // Check for magnet link
++ if let Ok(magnet_link) = MagnetURI::from_str(&text) {
++ let fallback = String::new();
++ let info_hash = magnet_link.info_hash().unwrap_or(&fallback);
++ let cm = FrgConnectionManager::default();
++
++ // Transmission lowercases info hashes internally
++ if cm
++ .client()
++ .torrents()
++ .torrent_by_hash(info_hash.to_lowercase())
++ .is_none()
++ {
++ debug!("Detected new magnet link: {}", &text);
++ window
++ .imp()
++ .magnet_link_notification(&text, magnet_link.name());
++ clear_clipboard = true;
++ } else {
++ debug!("Ignore magnet link, torrent is already added.");
++ }
+ }
+- }
+
+- // Check for torrent link
+- if let Ok(url) = Url::parse(&text) {
+- // We only support http/https, and no file links because of sandboxing
+- if url.path().ends_with(".torrent") && (url.scheme() == "http" || url.scheme() == "https") {
+- debug!("Detected torrent link: {}", &text);
+- window.imp().torrent_link_notification(&url);
+- clear_clipboard = true;
++ // Check for torrent link
++ if let Ok(url) = Url::parse(&text) {
++ // We only support http/https, and no file links because of
++ // sandboxing
++ if url.path().ends_with(".torrent")
++ && (url.scheme() == "http" || url.scheme() == "https")
++ {
++ debug!("Detected torrent link: {}", &text);
++ window.imp().torrent_link_notification(&url);
++ clear_clipboard = true;
++ }
+ }
+ }
+- }
+
+- if clear_clipboard {
+- // To avoid that the clipboard toast is shown multiple times, we clear the clipboard
+- clipboard.set_text("");
++ if clear_clipboard {
++ // To avoid that the clipboard toast is shown multiple times, we
++ // clear the clipboard
++ clipboard.set_text("");
++ }
+ }
+- });
++ );
+ glib::spawn_future_local(fut);
+ }
+ });
+
+ // torrent-added notification
+- cm.client().connect_local("torrent-added", false, clone!(@weak self as this => @default-return None, move |torrent|{
+- if settings_manager::boolean(Key::EnableNotificationsNewTorrent){
+- let torrent: TrTorrent = torrent[1].get().unwrap();
+- utils::system_notification(i18n("New torrent added"), torrent.name(), Some("folder-download-symbolic"));
+- }
+- None
+- }));
++ cm.client()
++ .connect_local("torrent-added", false, move |torrent| {
++ if settings_manager::boolean(Key::EnableNotificationsNewTorrent) {
++ let torrent: TrTorrent = torrent[1].get().unwrap();
++ utils::system_notification(
++ i18n("New torrent added"),
++ torrent.name(),
++ Some("folder-download-symbolic"),
++ );
++ }
++ None
++ });
+
+ // torrent-downloaded notification
+- cm.client().connect_local("torrent-downloaded", false, clone!(@weak self as this => @default-return None, move |torrent|{
+- if settings_manager::boolean(Key::EnableNotificationsDownloaded){
+- let torrent: TrTorrent = torrent[1].get().unwrap();
+- utils::system_notification(i18n("Torrent completely downloaded"), torrent.name(), Some("folder-download-symbolic"));
+- }
+- None
+- }));
++ cm.client()
++ .connect_local("torrent-downloaded", false, move |torrent| {
++ if settings_manager::boolean(Key::EnableNotificationsDownloaded) {
++ let torrent: TrTorrent = torrent[1].get().unwrap();
++ utils::system_notification(
++ i18n("Torrent completely downloaded"),
++ torrent.name(),
++ Some("folder-download-symbolic"),
++ );
++ }
++ None
++ });
+
+ // Lower polling rate when window isn't active
+- obj.connect_is_active_notify(clone!(@weak self as this => move |_| {
+- this.update_polling_mode();
+- }));
+-
+- obj.connect_suspended_notify(clone!(@weak self as this => move |_| {
+- this.update_polling_mode();
+- }));
++ obj.connect_is_active_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_polling_mode();
++ }
++ ));
++
++ obj.connect_suspended_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_polling_mode();
++ }
++ ));
+ }
+ }
+
+@@ -211,16 +253,21 @@ mod imp {
+ settings_manager::set_integer(Key::WindowWidth, width);
+ settings_manager::set_integer(Key::WindowHeight, height);
+
+- let fut = clone!(@weak window => async move {
+- window.set_visible(false);
++ let fut = clone!(
++ #[weak]
++ window,
++ async move {
++ window.set_visible(false);
++
++ // Wait till transmission daemon is stopped
++ FrgConnectionManager::default()
++ .disconnect()
++ .await
++ .expect("Unable to stop the transmission daemon");
+
+- // Wait till transmission daemon is stopped
+- FrgConnectionManager::default().disconnect()
+- .await
+- .expect("Unable to stop the transmission daemon");
+-
+- FrgApplication::default().quit();
+- });
++ FrgApplication::default().quit();
++ }
++ );
+ glib::spawn_future_local(fut);
+
+ glib::Propagation::Stop
+@@ -286,7 +333,7 @@ glib::wrapper! {
+ pub struct FrgApplicationWindow(
+ ObjectSubclass<imp::FrgApplicationWindow>)
+ @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow,
+- @implements gio::ActionMap, gio::ActionGroup;
++ @implements gtk::Root, gtk::Native, gtk::ShortcutManager, gio::ActionMap, gio::ActionGroup, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+ }
+
+ // FrgApplicationWindow implementation itself
+Index: fragments-3.0.1/src/utils.rs
+===================================================================
+--- fragments-3.0.1.orig/src/utils.rs
++++ fragments-3.0.1/src/utils.rs
+@@ -1,5 +1,5 @@
+ // Fragments - utils.rs
+-// Copyright (C) 2022-2023 Felix Häcker <haeckerfelix at gnome.org>
++// Copyright (C) 2022-2025 Felix Häcker <haeckerfelix at gnome.org>
+ //
+ // This program is free software: you can redistribute it and/or modify
+ // it under the terms of the GNU General Public License as published by
+@@ -18,7 +18,7 @@ use std::path::PathBuf;
+
+ use adw::prelude::*;
+ use gio::SimpleAction;
+-use glib::{clone, BindingFlags};
++use glib::BindingFlags;
+ use gtk::{gio, glib};
+ use transmission_gobject::{TrTorrent, TrTorrentStatus};
+
+@@ -166,21 +166,19 @@ pub fn session_dir_filechooser(parent: &
+ dialog.select_folder(
+ Some(parent),
+ gio::Cancellable::NONE,
+- clone!(@weak dialog, @weak parent => move |result| {
+- match result {
+- Ok(folder) => {
+- debug!("Selected directory: {:?}", folder.path());
+-
+- let session = FrgConnectionManager::default().client().session();
+- session.set_property(property_name, folder);
+-
+- FrgConnectionManager::default().check_directories();
+- }
+- Err(err) => {
+- warn!("Selected directory could not be accessed {:?}", err);
+- }
++ move |result| match result {
++ Ok(folder) => {
++ debug!("Selected directory: {:?}", folder.path());
++
++ let session = FrgConnectionManager::default().client().session();
++ session.set_property(property_name, folder);
++
++ FrgConnectionManager::default().check_directories();
++ }
++ Err(err) => {
++ warn!("Selected directory could not be accessed {:?}", err);
+ }
+- }),
++ },
+ );
+ }
+
+Index: fragments-3.0.1/src/app.rs
+===================================================================
+--- fragments-3.0.1.orig/src/app.rs
++++ fragments-3.0.1/src/app.rs
+@@ -90,20 +90,24 @@ mod imp {
+ }
+
+ // Connect to last used connection
+- let fut = clone!(@weak self as this => async move {
+- let cm = FrgConnectionManager::default();
++ let fut = clone!(
++ #[weak(rename_to = this)]
++ self,
++ async move {
++ let cm = FrgConnectionManager::default();
+
+- let local_connection = FrgConnection::default();
+- cm.connections().insert(0, &local_connection);
++ let local_connection = FrgConnection::default();
++ cm.connections().insert(0, &local_connection);
+
+- if let Err(err) = secret_store::open_keyring().await {
+- error!("Could not unlock keyring: {err}");
+- };
++ if let Err(err) = secret_store::open_keyring().await {
++ error!("Could not unlock keyring: {err}");
++ };
+
+- // Restore last used connection
+- let uuid = settings_manager::string(Key::ClientLastConnection);
+- this.set_connection(uuid).await;
+- });
++ // Restore last used connection
++ let uuid = settings_manager::string(Key::ClientLastConnection);
++ this.set_connection(uuid).await;
++ }
++ );
+ glib::spawn_future_local(fut);
+
+ // No window available -> we have to create one
+@@ -117,44 +121,60 @@ mod imp {
+
+ // app.add-torrent
+ let action = gio::SimpleAction::new("add-torrent", None);
+- action.connect_activate(clone!(@weak self as app => move |_, _| {
+- app.open_torrent_filechooser();
+- }));
++ action.connect_activate(clone!(
++ #[weak(rename_to = app)]
++ self,
++ move |_, _| {
++ app.open_torrent_filechooser();
++ }
++ ));
+ utils::bind_connected_property(&action);
+ obj.add_action(&action);
+ obj.set_accels_for_action("app.add-torrent", &["<primary>o"]);
+
+ // app.add-link
+ let action = gio::SimpleAction::new("add-link", string_ty);
+- action.connect_activate(clone!(@weak self as this => move |_, target| {
+- if let Some(torrent_link) = target.and_then(|x| x.get::<String>()) {
+- // Both magnet and torrent links are supported
+- this.add_torrent_by_link(torrent_link);
++ action.connect_activate(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, target| {
++ if let Some(torrent_link) = target.and_then(|x| x.get::<String>()) {
++ // Both magnet and torrent links are supported
++ this.add_torrent_by_link(torrent_link);
++ }
+ }
+- }));
++ ));
+ utils::bind_connected_property(&action);
+ obj.add_action(&action);
+
+ // app.search
+ let action = gio::SimpleAction::new("search", None);
+- action.connect_activate(clone!(@weak window => move |_, _| {
+- window.show_search(false);
+- }));
++ action.connect_activate(clone!(
++ #[weak]
++ window,
++ move |_, _| {
++ window.show_search(false);
++ }
++ ));
+ utils::bind_connected_property(&action);
+ obj.add_action(&action);
+ obj.set_accels_for_action("app.search", &["<primary>f"]);
+
+ // app.toggle-search
+ let action = gio::SimpleAction::new("toggle-search", None);
+- action.connect_activate(clone!(@weak window => move |_, _| {
+- window.show_search(true);
+- }));
++ action.connect_activate(clone!(
++ #[weak]
++ window,
++ move |_, _| {
++ window.show_search(true);
++ }
++ ));
+ utils::bind_connected_property(&action);
+ obj.add_action(&action);
+
+ // app.resume-torrents
+ let action = gio::SimpleAction::new("resume-torrents", None);
+- action.connect_activate(clone!(@weak self as app => move |_, _| {
++ action.connect_activate(move |_, _| {
+ let fut = async move {
+ let client = FrgConnectionManager::default().client();
+ if let Err(err) = client.start_torrents().await {
+@@ -162,12 +182,12 @@ mod imp {
+ };
+ };
+ glib::spawn_future_local(fut);
+- }));
++ });
+ obj.add_action(&action);
+
+ // app.pause-torrents
+ let action = gio::SimpleAction::new("pause-torrents", None);
+- action.connect_activate(clone!(@weak self as app => move |_, _| {
++ action.connect_activate(move |_, _| {
+ let fut = async move {
+ let client = FrgConnectionManager::default().client();
+ if let Err(err) = client.stop_torrents().await {
+@@ -175,73 +195,107 @@ mod imp {
+ };
+ };
+ glib::spawn_future_local(fut);
+-
+- }));
++ });
+ obj.add_action(&action);
+
+ // app.remove-torrents
+ let action = gio::SimpleAction::new("remove-torrents", None);
+- action.connect_activate(clone!(@weak self as this => move |_, _| {
+- this.show_remove_torrents_dialog();
+- }));
++ action.connect_activate(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, _| {
++ this.show_remove_torrents_dialog();
++ }
++ ));
+ obj.add_action(&action);
+
+ // app.show-stats
+ let action = gio::SimpleAction::new("show-stats", None);
+- action.connect_activate(clone!(@weak window => move |_, _| {
+- FrgStatsDialog::new().present(Some(&window));
+- }));
++ action.connect_activate(clone!(
++ #[weak]
++ window,
++ move |_, _| {
++ FrgStatsDialog::new().present(Some(&window));
++ }
++ ));
+ utils::bind_connected_property(&action);
+ obj.add_action(&action);
+
+ // app.set-connection
+ let action = gio::SimpleAction::new("set-connection", string_ty);
+- action.connect_activate(clone!(@weak self as this => move |_, target| {
+- if let Some(connection_uuid) = target.and_then(|x| x.get::<String>()) {
+- let fut = async move {
+- this.set_connection(connection_uuid).await;
+- };
+- glib::spawn_future_local(fut);
++ action.connect_activate(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, target| {
++ if let Some(connection_uuid) = target.and_then(|x| x.get::<String>()) {
++ let fut = async move {
++ this.set_connection(connection_uuid).await;
++ };
++ glib::spawn_future_local(fut);
++ }
+ }
+- }));
++ ));
+ obj.add_action(&action);
+
+ // app.reconnect
+ let action = gio::SimpleAction::new("reconnect", None);
+- action.connect_activate(clone!(@weak self as this => move |_, _| {
+- let cm = FrgConnectionManager::default();
+- this.obj().activate_action("set-connection", Some(&cm.current_connection().uuid().to_variant()));
+- }));
++ action.connect_activate(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_, _| {
++ let cm = FrgConnectionManager::default();
++ this.obj().activate_action(
++ "set-connection",
++ Some(&cm.current_connection().uuid().to_variant()),
++ );
++ }
++ ));
+ obj.add_action(&action);
+
+ // app.add-remote-connection
+ let action = gio::SimpleAction::new("add-remote-connection", None);
+- action.connect_activate(clone!(@weak window => move |_, _| {
+- let dialog = FrgAddConnectionDialog::new();
+- dialog.present(Some(&window));
+- }));
++ action.connect_activate(clone!(
++ #[weak]
++ window,
++ move |_, _| {
++ let dialog = FrgAddConnectionDialog::new();
++ dialog.present(Some(&window));
++ }
++ ));
+ obj.add_action(&action);
+
+ // app.show-preferences
+ let action = gio::SimpleAction::new("show-preferences", None);
+- action.connect_activate(clone!(@weak window => move |_, _| {
+- FrgPreferencesDialog::default().present(Some(&window));
+- }));
++ action.connect_activate(clone!(
++ #[weak]
++ window,
++ move |_, _| {
++ FrgPreferencesDialog::default().present(Some(&window));
++ }
++ ));
+ obj.set_accels_for_action("app.show-preferences", &["<primary>comma"]);
+ obj.add_action(&action);
+
+ // app.about
+ let action = gio::SimpleAction::new("about", None);
+- action.connect_activate(clone!(@weak window => move |_, _| {
+- about_dialog::show(&window);
+- }));
++ action.connect_activate(clone!(
++ #[weak]
++ window,
++ move |_, _| {
++ about_dialog::show(&window);
++ }
++ ));
+ obj.add_action(&action);
+
+ // app.quit
+ let action = gio::SimpleAction::new("quit", None);
+- action.connect_activate(clone!(@weak window => move |_, _| {
+- window.close();
+- }));
++ action.connect_activate(clone!(
++ #[weak]
++ window,
++ move |_, _| {
++ window.close();
++ }
++ ));
+ obj.set_accels_for_action("app.quit", &["<primary>q"]);
+ obj.add_action(&action);
+
+@@ -249,25 +303,45 @@ mod imp {
+
+ // Update state of `pause-torrents` and `remove-torrents` action
+ let stats = FrgConnectionManager::default().client().session_stats();
+- stats.connect_active_torrent_count_notify(clone!(@weak self as this => move |_|{
+- this.obj().update_inhibit_state();
+- }));
+- stats.connect_downloaded_torrent_count_notify(clone!(@weak self as this => move |_|{
+- this.update_session_gactions();
+- this.obj().update_inhibit_state();
+- }));
+- stats.connect_paused_torrent_count_notify(clone!(@weak self as this => move |_|{
+- this.update_session_gactions();
+- this.obj().update_inhibit_state();
+- }));
+- stats.connect_torrent_count_notify(clone!(@weak self as this => move |_|{
+- this.update_session_gactions();
+- }));
++ stats.connect_active_torrent_count_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.obj().update_inhibit_state();
++ }
++ ));
++ stats.connect_downloaded_torrent_count_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_session_gactions();
++ this.obj().update_inhibit_state();
++ }
++ ));
++ stats.connect_paused_torrent_count_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_session_gactions();
++ this.obj().update_inhibit_state();
++ }
++ ));
++ stats.connect_torrent_count_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_session_gactions();
++ }
++ ));
+
+ let client = FrgConnectionManager::default().client();
+- client.connect_is_connected_notify(clone!(@weak self as this => move |_|{
+- this.update_session_gactions();
+- }));
++ client.connect_is_connected_notify(clone!(
++ #[weak(rename_to = this)]
++ self,
++ move |_| {
++ this.update_session_gactions();
++ }
++ ));
+
+ self.update_session_gactions();
+ }
+@@ -285,8 +359,14 @@ mod imp {
+ // Wait till we're connected with something to add the files
+ let signal_id: Rc<RefCell<Option<glib::SignalHandlerId>>> = Rc::default();
+ let files = files.to_vec().clone();
+- *signal_id.borrow_mut() = Some(client.connect_is_connected_notify(
+- clone!(@weak application, @strong signal_id, @strong files => move |client| {
++ *signal_id.borrow_mut() = Some(client.connect_is_connected_notify(clone!(
++ #[weak]
++ application,
++ #[strong]
++ signal_id,
++ #[strong]
++ files,
++ move |client| {
+ if client.is_connected() {
+ application.add_torrents_from_files(&files);
+
+@@ -294,8 +374,8 @@ mod imp {
+ let signal_id = signal_id.borrow_mut().take().unwrap();
+ glib::object::ObjectExt::disconnect(client, signal_id);
+ }
+- }),
+- ));
++ }
++ )));
+ }
+ }
+ }
+@@ -387,32 +467,46 @@ mod imp {
+ }
+ }
+
+- let fut = clone!(@strong file => async move {
+- match fs::read(&file).await {
+- Ok(content) => {
+- let encoded = STANDARD.encode(content);
+-
+- let cm = FrgConnectionManager::default();
+- let res = cm.add_torrent_by_metainfo(encoded).await;
+- if let Err(err) = res {
+- debug!("Could not add file torrent: {:?}", err);
+- utils::inapp_notification("Unable to add file torrent", gtk::Widget::NONE);
+- }
++ let fut = clone!(
++ #[strong]
++ file,
++ async move {
++ match fs::read(&file).await {
++ Ok(content) => {
++ let encoded = STANDARD.encode(content);
++
++ let cm = FrgConnectionManager::default();
++ let res = cm.add_torrent_by_metainfo(encoded).await;
++ if let Err(err) = res {
++ debug!("Could not add file torrent: {:?}", err);
++ utils::inapp_notification(
++ "Unable to add file torrent",
++ gtk::Widget::NONE,
++ );
++ }
+
+- if settings_manager::boolean(Key::TrashOriginalTorrentFiles){
+- let gfile = gio::File::for_path(file);
+- if let Err(err) = gfile.trash_future(glib::Priority::DEFAULT).await {
+- debug!("Unable to trash torrent file: {:?}", err);
+- utils::inapp_notification("Unable to trash torrent file", gtk::Widget::NONE);
++ if settings_manager::boolean(Key::TrashOriginalTorrentFiles) {
++ let gfile = gio::File::for_path(file);
++ if let Err(err) = gfile.trash_future(glib::Priority::DEFAULT).await
++ {
++ debug!("Unable to trash torrent file: {:?}", err);
++ utils::inapp_notification(
++ "Unable to trash torrent file",
++ gtk::Widget::NONE,
++ );
++ }
+ }
+ }
+- }
+- Err(err) => {
+- debug!("Unable to read file content: {:?}", err);
+- utils::inapp_notification("Unable to read file content", gtk::Widget::NONE);
++ Err(err) => {
++ debug!("Unable to read file content: {:?}", err);
++ utils::inapp_notification(
++ "Unable to read file content",
++ gtk::Widget::NONE,
++ );
++ }
+ }
+ }
+- });
++ );
+
+ glib::spawn_future_local(fut);
+ }
+@@ -460,16 +554,22 @@ mod imp {
+ dialog.open_multiple(
+ Some(&window),
+ gio::Cancellable::NONE,
+- clone!(@strong dialog, @weak self as this => move |result| {
+- if let Ok(files) = result {
+- let files = files
+- .snapshot()
+- .iter()
+- .map(|o| o.downcast_ref::<gio::File>().unwrap().clone())
+- .collect();
+- this.obj().add_torrents_from_files(&files);
++ clone!(
++ #[strong(rename_to = _dialog)]
++ dialog,
++ #[weak(rename_to = this)]
++ self,
++ move |result| {
++ if let Ok(files) = result {
++ let files = files
++ .snapshot()
++ .iter()
++ .map(|o| o.downcast_ref::<gio::File>().unwrap().clone())
++ .collect();
++ this.obj().add_torrents_from_files(&files);
++ }
+ }
+- }),
++ ),
+ );
+ }
+
- Previous message (by thread): Bug#1115757: fragments: FTBFS: unsatisfiable build-dependencies: librust-ashpd-0.11+async-std-dev, librust-ashpd-0.11+gtk4-dev, librust-gtk4-0.9+default-dev, librust-gtk4-0.9+gnome-47-dev, librust-libadwaita-0.7+default-dev, librust-libadwaita-0.7+v1-7-dev
- Next message (by thread): Bug#1115757: fragments: FTBFS: unsatisfiable build-dependencies: librust-ashpd-0.11+async-std-dev, librust-ashpd-0.11+gtk4-dev, librust-gtk4-0.9+default-dev, librust-gtk4-0.9+gnome-47-dev, librust-libadwaita-0.7+default-dev, librust-libadwaita-0.7+v1-7-dev
- Messages sorted by:
[ date ]
[ thread ]
[ subject ]
[ author ]
More information about the pkg-gnome-maintainers
mailing list