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


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::<&gtk::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(&copy_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);
++                        }
+                     }
+-                }),
++                ),
+             );
+         }
+ 


More information about the pkg-gnome-maintainers mailing list