@@ -4,7 +4,8 @@ use gtk::glib::{clone, BoxedAnyObject};
44use gtk:: { gio, glib, pango} ;
55
66use crate :: container:: Container ;
7- use crate :: distrobox:: ExportableApp ;
7+ use crate :: distrobox:: { ExportableApp , ExportableBinary } ;
8+ use crate :: gtk_utils:: reaction;
89
910use std:: cell:: RefCell ;
1011
@@ -20,19 +21,22 @@ mod imp {
2021 #[ property( get, set) ]
2122 pub container : RefCell < Container > ,
2223 pub dialog : adw:: Dialog ,
24+ pub toast_overlay : adw:: ToastOverlay ,
2325 pub toolbar_view : adw:: ToolbarView ,
2426 pub content : gtk:: Box ,
2527 pub scrolled_window : gtk:: ScrolledWindow ,
2628 pub stack : gtk:: Stack ,
2729 pub error_label : gtk:: Label ,
2830 pub list_box : gtk:: ListBox ,
31+ pub binaries_list_box : gtk:: ListBox ,
32+ pub binary_name_entry : adw:: EntryRow ,
2933 }
3034
3135 #[ derived_properties]
3236 impl ObjectImpl for ExportableAppsDialog {
3337 fn constructed ( & self ) {
3438 let obj = self . obj ( ) ;
35- obj. set_title ( "Exportable Apps " ) ;
39+ obj. set_title ( "Manage Exports " ) ;
3640 obj. set_content_width ( 360 ) ;
3741 obj. set_content_height ( 640 ) ;
3842
@@ -52,9 +56,9 @@ mod imp {
5256 self . stack . add_named ( & self . error_label , Some ( "error" ) ) ;
5357
5458 let loading_page = adw:: StatusPage :: new ( ) ;
55- loading_page. set_title ( "Loading App List " ) ;
59+ loading_page. set_title ( "Loading Exports " ) ;
5660 loading_page. set_description ( Some (
57- "Please wait while we load the list of exportable apps. This may take some time if the distrobox wasn't running" ,
61+ "Please wait while we load the list of exportable apps and binaries . This may take some time if the distrobox wasn't running" ,
5862 ) ) ;
5963 loading_page. set_child ( Some ( & adw:: Spinner :: new ( ) ) ) ;
6064 self . stack . add_named ( & loading_page, Some ( "loading" ) ) ;
@@ -68,16 +72,41 @@ mod imp {
6872 export_apps_group. set_margin_bottom ( 12 ) ;
6973 export_apps_group. set_title ( "Exportable Apps" ) ;
7074 export_apps_group. add ( & self . list_box ) ;
71- self . stack . add_named ( & export_apps_group, Some ( "apps" ) ) ;
75+
76+ // Setup binary export input
77+ self . binary_name_entry . set_title ( "Export New Binary" ) ;
78+ self . binary_name_entry . set_show_apply_button ( true ) ;
79+ self . binary_name_entry
80+ . add_css_class ( "add-binary-entry-row" ) ;
81+
82+ self . binaries_list_box . add_css_class ( "boxed-list" ) ;
83+ self . binaries_list_box . set_selection_mode ( gtk:: SelectionMode :: None ) ;
84+ self . binaries_list_box . set_margin_top ( 12 ) ;
85+
86+ let export_binaries_group = adw:: PreferencesGroup :: new ( ) ;
87+ export_binaries_group. set_margin_start ( 12 ) ;
88+ export_binaries_group. set_margin_end ( 12 ) ;
89+ export_binaries_group. set_margin_top ( 0 ) ;
90+ export_binaries_group. set_margin_bottom ( 12 ) ;
91+ export_binaries_group. set_title ( "Exported Binaries" ) ;
92+ export_binaries_group. add ( & self . binary_name_entry ) ;
93+ export_binaries_group. add ( & self . binaries_list_box ) ;
94+
95+ let content_box = gtk:: Box :: new ( gtk:: Orientation :: Vertical , 0 ) ;
96+ content_box. append ( & export_apps_group) ;
97+ content_box. append ( & export_binaries_group) ;
98+ self . stack . add_named ( & content_box, Some ( "apps" ) ) ;
7299
73100 let empty_page = adw:: StatusPage :: new ( ) ;
74- empty_page. set_title ( "No Exportable Apps" ) ;
101+ empty_page. set_title ( "No Exportable Items" ) ;
102+ empty_page. set_description ( Some ( "No applications or binaries found in this container" ) ) ;
75103
76104 self . stack . add_named ( & empty_page, Some ( "empty" ) ) ;
77105
78106 self . content . append ( & self . scrolled_window ) ;
79107 self . toolbar_view . set_content ( Some ( & self . content ) ) ;
80- self . obj ( ) . set_child ( Some ( & self . toolbar_view ) ) ;
108+ self . toast_overlay . set_child ( Some ( & self . toolbar_view ) ) ;
109+ self . obj ( ) . set_child ( Some ( & self . toast_overlay ) ) ;
81110 }
82111 }
83112
@@ -104,6 +133,22 @@ mod imp {
104133 this. container ( ) . unexport ( file_path) ;
105134 } ,
106135 ) ;
136+ klass. install_action (
137+ "dialog.export-binary" ,
138+ Some ( VariantTy :: STRING ) ,
139+ |this, _action, target| {
140+ let binary_path = target. unwrap ( ) . str ( ) . unwrap ( ) ;
141+ this. container ( ) . export_binary ( binary_path) ;
142+ } ,
143+ ) ;
144+ klass. install_action (
145+ "dialog.unexport-binary" ,
146+ Some ( VariantTy :: STRING ) ,
147+ |this, _action, target| {
148+ let binary_path = target. unwrap ( ) . str ( ) . unwrap ( ) ;
149+ this. container ( ) . unexport_binary ( binary_path) ;
150+ } ,
151+ ) ;
107152 }
108153 }
109154
@@ -121,43 +166,119 @@ impl ExportableAppsDialog {
121166 . property ( "container" , container)
122167 . build ( ) ;
123168
169+ let apps = this. container ( ) . apps ( ) ;
170+ let binaries = this. container ( ) . binaries ( ) ;
171+
172+ let is_empty = move || -> bool {
173+ let n_apps = apps. data :: < gio:: ListStore > ( ) . map ( |s| s. n_items ( ) ) . unwrap_or ( 0 ) ;
174+ let n_binaries = binaries. data :: < gio:: ListStore > ( ) . map ( |s| s. n_items ( ) ) . unwrap_or ( 0 ) ;
175+ n_apps == 0 && n_binaries == 0
176+ } ;
177+
124178 let this_clone = this. clone ( ) ;
125- container. apps ( ) . connect_loading_notify ( move |resource| {
126- if resource. loading ( ) {
127- this_clone. imp ( ) . stack . set_visible_child_name ( "loading" ) ;
128- }
129- } ) ;
130- let this_clone = this. clone ( ) ;
131- container. apps ( ) . connect_error_notify ( move |resource| {
132- if let Some ( err) = resource. error ( ) {
133- this_clone. imp ( ) . error_label . set_label ( & err) ;
134- this_clone. imp ( ) . stack . set_visible_child_name ( "error" ) ;
179+ let apps = this. container ( ) . apps ( ) ;
180+ let binaries = this. container ( ) . binaries ( ) ;
181+ reaction ! {
182+ ( apps. error( ) , binaries. error( ) ) ,
183+ move |( e1, e2) : ( Option <String >, Option <String >) | {
184+ if let Some ( err) = e1. or( e2) {
185+ this_clone. imp( ) . error_label. set_label( & err) ;
186+ this_clone. imp( ) . stack. set_visible_child_name( "error" ) ;
187+ }
135188 }
136- } ) ;
189+ } ;
190+
137191 let this_clone = this. clone ( ) ;
138- container. apps ( ) . connect_data_changed ( move |resource| {
139- let apps = resource. data :: < gio:: ListStore > ( ) . unwrap ( ) ;
140-
141- if apps. n_items ( ) == 0 {
142- this_clone. imp ( ) . stack . set_visible_child_name ( "empty" ) ;
143- return ;
144- }
192+ let apps = this. container ( ) . apps ( ) ;
193+ let render_apps = move || {
194+ let apps = apps. data :: < gio:: ListStore > ( ) ;
145195
146196 this_clone. imp ( ) . stack . set_visible_child_name ( "apps" ) ;
147-
148197 let this = this_clone. clone ( ) ;
149198 this_clone
150199 . imp ( )
151200 . list_box
152- . bind_model ( Some ( & apps) , move |obj| {
201+ . bind_model ( apps. as_ref ( ) , move |obj| {
153202 let app = obj
154203 . downcast_ref :: < BoxedAnyObject > ( )
155204 . map ( |obj| obj. borrow :: < ExportableApp > ( ) )
156205 . unwrap ( ) ;
157206 this. build_row ( & app) . upcast ( )
158207 } ) ;
159- } ) ;
208+
209+ } ;
210+
211+ let this_clone = this. clone ( ) ;
212+ let binaries = this. container ( ) . binaries ( ) ;
213+ let render_binaries = move || {
214+ let binaries = binaries. data :: < gio:: ListStore > ( ) ;
215+
216+ this_clone. imp ( ) . stack . set_visible_child_name ( "apps" ) ;
217+ let this = this_clone. clone ( ) ;
218+ this_clone
219+ . imp ( )
220+ . binaries_list_box
221+ . bind_model ( binaries. as_ref ( ) , move |obj| {
222+ let binary = obj
223+ . downcast_ref :: < BoxedAnyObject > ( )
224+ . map ( |obj| obj. borrow :: < ExportableBinary > ( ) )
225+ . unwrap ( ) ;
226+ this. build_binary_row ( & binary) . upcast ( )
227+ } ) ;
228+ } ;
229+
230+ let this_clone = this. clone ( ) ;
231+ let apps = this. container ( ) . apps ( ) ;
232+ let binaries = this. container ( ) . binaries ( ) ;
233+ reaction ! {
234+ ( apps. loading( ) , binaries. loading( ) ) ,
235+ move |( b1, b2) : ( bool , bool ) | {
236+ if b1 || b2 {
237+ this_clone. imp( ) . stack. set_visible_child_name( "loading" ) ;
238+ } else if is_empty( ) {
239+ this_clone. imp( ) . stack. set_visible_child_name( "empty" ) ;
240+ } else {
241+ render_apps( ) ;
242+ render_binaries( ) ;
243+ }
244+ }
245+ } ;
246+
247+
248+ // Connect the binary name entry apply signal
249+ let this_clone = this. clone ( ) ;
250+ this. imp ( )
251+ . binary_name_entry
252+ . connect_apply ( move |entry| {
253+ let binary_name = entry. text ( ) . to_string ( ) ;
254+ if !binary_name. is_empty ( ) {
255+ let task = this_clone. container ( ) . export_binary ( & binary_name) ;
256+ entry. set_text ( "" ) ;
257+
258+ // Monitor task status to show error toasts
259+ let this = this_clone. clone ( ) ;
260+ let binary_name_clone = binary_name. clone ( ) ;
261+ reaction ! ( task. status( ) , move |status: String | {
262+ match status. as_str( ) {
263+ "failed" => {
264+ let error_ref = task. error( ) ;
265+ let error_msg = if let Some ( err) = error_ref. as_ref( ) {
266+ format!( "Failed to export '{}': {}" , binary_name_clone, err)
267+ } else {
268+ format!( "Failed to export '{}'" , binary_name_clone)
269+ } ;
270+ let toast = adw:: Toast :: new( & error_msg) ;
271+ toast. set_timeout( 5 ) ;
272+ this. imp( ) . toast_overlay. add_toast( toast) ;
273+ }
274+ _ => { }
275+ }
276+ } ) ;
277+ }
278+ } ) ;
279+
160280 container. apps ( ) . reload ( ) ;
281+ container. binaries ( ) . reload ( ) ;
161282
162283 this
163284 }
@@ -212,4 +333,37 @@ impl ExportableAppsDialog {
212333
213334 row
214335 }
336+
337+ pub fn build_binary_row ( & self , binary : & ExportableBinary ) -> adw:: ActionRow {
338+ // Create the action row
339+ let row = adw:: ActionRow :: new ( ) ;
340+ row. set_title ( & binary. name ) ;
341+ row. set_subtitle ( & binary. source_path ) ;
342+
343+ // Create the menu button
344+ let menu_button = gtk:: MenuButton :: new ( ) ;
345+ menu_button. set_icon_name ( "view-more-symbolic" ) ;
346+ menu_button. set_valign ( gtk:: Align :: Center ) ;
347+ menu_button. add_css_class ( "flat" ) ;
348+
349+ // Create the menu model - only show unexport since we're only showing exported binaries
350+ let menu_model = gio:: Menu :: new ( ) ;
351+ let unexport_action = gio:: MenuItem :: new (
352+ Some ( "Unexport Binary" ) ,
353+ Some ( & format ! (
354+ "dialog.unexport-binary(\" {}\" )" ,
355+ binary. source_path
356+ ) ) ,
357+ ) ;
358+ menu_model. append_item ( & unexport_action) ;
359+
360+ // Set up the popover menu
361+ let popover = gtk:: PopoverMenu :: from_model ( Some ( & menu_model) ) ;
362+ menu_button. set_popover ( Some ( & popover) ) ;
363+
364+ // Add the menu button to the action row
365+ row. add_suffix ( & menu_button) ;
366+
367+ row
368+ }
215369}
0 commit comments