@@ -79,32 +79,46 @@ class GDriveActionSignals(QObject):
7979 # Signal can only take simple types, so we can't use `set[str]`
8080 finished = Signal (set ) # the impacted folder ids
8181 error = Signal (str ) # the error message
82+ started = Signal ()
83+ status_changed = Signal ()
8284
8385class GDriveAction (QRunnable ):
84- def __init__ (self ):
86+ def __init__ (self , description : str ):
8587 super ().__init__ ()
8688 self .signals = GDriveActionSignals ()
8789 self .impacted_folders : set [str ] = set ()
90+ self .description = description
91+ self .status = "pending" # pending, running, completed, error
92+ self .error_message = None
8893
8994 @Slot ()
9095 def run (self ):
96+ self .status = "running"
97+ self .signals .started .emit ()
98+ self .signals .status_changed .emit ()
9199 try :
92100 self .execute ()
101+ self .status = "completed"
93102 self .signals .finished .emit (self .impacted_folders )
103+ self .signals .status_changed .emit ()
94104 except Exception as e :
105+ self .status = "error"
106+ self .error_message = str (e )
95107 self .signals .error .emit (str (e ))
108+ self .signals .status_changed .emit ()
96109
97110 def execute (self ):
98111 raise NotImplementedError
99112
100113class RenameAction (GDriveAction ):
101114 def __init__ (self , gcache : DriveCache , file_id : str , new_name : str ):
102- super ().__init__ ()
115+ item = gcache .get_item (file_id )
116+ old_name = item ['name' ] if item else file_id
117+ super ().__init__ (f"Renaming '{ old_name } ' to '{ new_name } '" )
103118 self .gcache = gcache
104119 self .file_id = file_id
105120 self .new_name = new_name
106121
107- item = self .gcache .get_item (file_id )
108122 if item :
109123 if item .get ('parent_id' ):
110124 self .impacted_folders .add (item ['parent_id' ])
@@ -116,7 +130,18 @@ def execute(self):
116130
117131class MoveAction (GDriveAction ):
118132 def __init__ (self , gcache : DriveCache , file_id : str , destination : str | tuple [str | None , str | None ], previous_parents : list [str ] | None = None ):
119- super ().__init__ ()
133+ item = gcache .get_item (file_id )
134+ name = item ['name' ] if item else file_id
135+
136+ dest_name = "folder"
137+ if isinstance (destination , str ):
138+ dest_item = gcache .get_item (destination )
139+ if dest_item :
140+ dest_name = f"'{ dest_item ['name' ]} '"
141+ elif isinstance (destination , tuple ):
142+ dest_name = "selected folder"
143+
144+ super ().__init__ (f"Moving '{ name } ' to { dest_name } " )
120145 self .gcache = gcache
121146 self .file_id = file_id
122147 self .destination = destination
@@ -125,7 +150,6 @@ def __init__(self, gcache: DriveCache, file_id: str, destination: str | tuple[st
125150 if previous_parents :
126151 self .impacted_folders .update (previous_parents )
127152 else :
128- item = self .gcache .get_item (file_id )
129153 if item and item .get ('parent_id' ):
130154 self .impacted_folders .add (item ['parent_id' ])
131155
@@ -144,7 +168,7 @@ def execute(self):
144168
145169class CreateFolderAction (GDriveAction ):
146170 def __init__ (self , gcache : DriveCache , parent_id : str , folder_name : str ):
147- super ().__init__ ()
171+ super ().__init__ (f"Creating folder ' { folder_name } '" )
148172 self .gcache = gcache
149173 self .parent_id = parent_id
150174 self .folder_name = folder_name
@@ -463,11 +487,14 @@ def _restore_icon(self, item):
463487 self .original_icon = None
464488
465489class PieProgressBar (QWidget ):
490+ clicked = Signal ()
491+
466492 def __init__ (self , parent = None ):
467493 super ().__init__ (parent )
468494 self .setFixedSize (24 , 24 )
469495 self ._value = 0
470496 self ._maximum = 1
497+ self .setCursor (Qt .CursorShape .PointingHandCursor )
471498
472499 def setValue (self , value ):
473500 self ._value = value
@@ -477,6 +504,11 @@ def setMaximum(self, maximum):
477504 self ._maximum = maximum
478505 self .update ()
479506
507+ def mousePressEvent (self , event ):
508+ if event .button () == Qt .MouseButton .LeftButton :
509+ self .clicked .emit ()
510+ super ().mousePressEvent (event )
511+
480512 def paintEvent (self , event ):
481513 painter = QPainter (self )
482514 painter .setRenderHint (QPainter .RenderHint .Antialiasing )
@@ -509,6 +541,118 @@ def paintEvent(self, event):
509541 painter .setBrush (Qt .BrushStyle .NoBrush )
510542 painter .drawEllipse (rect )
511543
544+ class GDriveProgressPopover (QDialog ):
545+ def __init__ (self , parent , actions : list [GDriveAction ]):
546+ super ().__init__ (parent , Qt .WindowType .Popup | Qt .WindowType .FramelessWindowHint )
547+ self .actions = actions
548+ self .setMinimumWidth (350 )
549+ self .setMaximumHeight (400 )
550+ self .init_ui ()
551+
552+ def init_ui (self ):
553+ layout = QVBoxLayout (self )
554+ layout .setContentsMargins (1 , 1 , 1 , 1 ) # Small border
555+
556+ self .container = QWidget ()
557+ self .container .setObjectName ("popoverContainer" )
558+ self .container .setStyleSheet ("""
559+ QWidget#popoverContainer {
560+ background-color: palette(window);
561+ border: 1px solid palette(mid);
562+ border-radius: 8px;
563+ }
564+ """ )
565+ container_layout = QVBoxLayout (self .container )
566+
567+ header = QHBoxLayout ()
568+ title = QLabel ("Google Drive Operations" )
569+ title .setStyleSheet ("font-weight: bold; font-size: 14px; margin: 5px;" )
570+ header .addWidget (title )
571+ header .addStretch ()
572+
573+ clear_btn = QPushButton ("Clear Completed" )
574+ clear_btn .setStyleSheet ("font-size: 11px;" )
575+ clear_btn .clicked .connect (self .clear_completed )
576+ header .addWidget (clear_btn )
577+
578+ container_layout .addLayout (header )
579+
580+ self .list_widget = QListWidget ()
581+ self .list_widget .setStyleSheet ("""
582+ QListWidget {
583+ border: none;
584+ background-color: transparent;
585+ }
586+ QListWidget::item {
587+ border-bottom: 1px solid palette(alternate-base);
588+ }
589+ """ )
590+ container_layout .addWidget (self .list_widget )
591+
592+ layout .addWidget (self .container )
593+
594+ self .refresh_list ()
595+
596+ def clear_completed (self ):
597+ # We need to notify the parent to actually clear them from the list
598+ self .parent ().clear_completed_actions ()
599+ self .refresh_list ()
600+
601+ def refresh_list (self ):
602+ self .list_widget .clear ()
603+ if not self .actions :
604+ item = QListWidgetItem ("No active operations" )
605+ item .setTextAlignment (Qt .AlignmentFlag .AlignCenter )
606+ item .setFlags (Qt .ItemFlag .NoItemFlags )
607+ self .list_widget .addItem (item )
608+ return
609+
610+ # Show most recent first
611+ for action in reversed (self .actions ):
612+ item = QListWidgetItem ()
613+ widget = QWidget ()
614+ item_layout = QHBoxLayout (widget )
615+ item_layout .setContentsMargins (8 , 8 , 8 , 8 )
616+
617+ icon_label = QLabel ()
618+ status_color = None
619+ if action .status == "pending" :
620+ icon = get_icon (OutlineIcon .CLOCK )
621+ elif action .status == "running" :
622+ icon = get_icon (OutlineIcon .LOADER_2 )
623+ elif action .status == "completed" :
624+ icon = get_icon (OutlineIcon .CIRCLE_CHECK , color = "#28a745" )
625+ status_color = "#28a745"
626+ elif action .status == "error" :
627+ icon = get_icon (OutlineIcon .CIRCLE_X , color = "#dc3545" )
628+ status_color = "#dc3545"
629+ else :
630+ icon = get_icon (OutlineIcon .QUESTION_MARK )
631+
632+ icon_label .setPixmap (icon .pixmap (24 , 24 ))
633+ item_layout .addWidget (icon_label )
634+
635+ text_layout = QVBoxLayout ()
636+ desc_label = QLabel (action .description )
637+ desc_label .setWordWrap (True )
638+ text_layout .addWidget (desc_label )
639+
640+ status_text = action .status .capitalize ()
641+ status_label = QLabel (status_text )
642+ status_label .setStyleSheet (f"font-size: 10px; color: { status_color if status_color else 'palette(text)' } ;" )
643+ text_layout .addWidget (status_label )
644+
645+ if action .status == "error" and action .error_message :
646+ error_label = QLabel (action .error_message )
647+ error_label .setStyleSheet ("color: #dc3545; font-size: 9px;" )
648+ error_label .setWordWrap (True )
649+ text_layout .addWidget (error_label )
650+
651+ item_layout .addLayout (text_layout )
652+
653+ item .setSizeHint (widget .sizeHint ())
654+ self .list_widget .addItem (item )
655+ self .list_widget .setItemWidget (item , widget )
512656
513657class ClickSelectLineEdit (QLineEdit ):
514658 escPressed = Signal ()
@@ -556,6 +700,8 @@ def __init__(self):
556700 self .gdrive_pool .setMaxThreadCount (10 )
557701 self .gdrive_tasks_total = 0
558702 self .gdrive_tasks_completed = 0
703+ self .gdrive_actions : list [GDriveAction ] = []
704+ self .progress_popover = None
559705
560706 self .gcache : DriveCache | None = None
561707 self .init_ui ()
@@ -644,6 +790,7 @@ def init_ui(self):
644790 top_bar .addWidget (self .address_bar )
645791
646792 self .gdrive_progress_widget = PieProgressBar ()
793+ self .gdrive_progress_widget .clicked .connect (self .toggle_progress_popover )
647794 top_bar .addWidget (self .gdrive_progress_widget )
648795
649796 self .folder_menu_btn = QPushButton ()
@@ -1140,14 +1287,48 @@ def _do_item_dropped(self, source_datas: list[dict[str, Any]], target_data: dict
11401287 action = MoveAction (self .gcache , source_data ['id' ], target_data ['id' ], previous_parents = previous_parents )
11411288 self .queue_gdrive_action (action )
11421289
1290+ def toggle_progress_popover (self ):
1291+ if self .progress_popover and self .progress_popover .isVisible ():
1292+ self .progress_popover .close ()
1293+ return
1294+
1295+ if not self .progress_popover :
1296+ self .progress_popover = GDriveProgressPopover (self , self .gdrive_actions )
1297+
1298+ # Position below the progress widget
1299+ pos = self .gdrive_progress_widget .mapToGlobal (self .gdrive_progress_widget .rect ().bottomLeft ())
1300+ # Offset to center it a bit better under the widget
1301+ pos .setX (pos .x () - self .progress_popover .minimumWidth () // 2 + self .gdrive_progress_widget .width () // 2 )
1302+ # Ensure it doesn't go off screen
1303+ screen_geo = self .screen ().geometry ()
1304+ if pos .x () + self .progress_popover .width () > screen_geo .right ():
1305+ pos .setX (screen_geo .right () - self .progress_popover .width () - 10 )
1306+ if pos .x () < screen_geo .left ():
1307+ pos .setX (screen_geo .left () + 10 )
1308+
1309+ self .progress_popover .move (pos )
1310+ self .progress_popover .refresh_list ()
1311+ self .progress_popover .show ()
1312+
1313+ def clear_completed_actions (self ):
1314+ self .gdrive_actions = [a for a in self .gdrive_actions if a .status not in ("completed" , "error" )]
1315+ if self .progress_popover :
1316+ self .progress_popover .actions = self .gdrive_actions
1317+
11431318 def queue_gdrive_action (self , action : GDriveAction ):
11441319 action .signals .finished .connect (self ._on_gdrive_action_finished )
11451320 action .signals .error .connect (self ._on_gdrive_action_error )
1321+ action .signals .status_changed .connect (self ._on_action_status_changed )
11461322
1323+ self .gdrive_actions .append (action )
11471324 self .gdrive_tasks_total += 1
11481325 self ._update_gdrive_progress ()
11491326 self .gdrive_pool .start (action )
11501327
1328+ def _on_action_status_changed (self ):
1329+ if self .progress_popover and self .progress_popover .isVisible ():
1330+ self .progress_popover .refresh_list ()
1331+
11511332 def _update_gdrive_progress (self ):
11521333 if self .gdrive_tasks_total > self .gdrive_tasks_completed :
11531334 self .gdrive_progress_widget .setMaximum (self .gdrive_tasks_total )
0 commit comments