Skip to content

Commit 36e192d

Browse files
committed
App: add basic operations list
[skip ci]
1 parent 674d921 commit 36e192d

1 file changed

Lines changed: 187 additions & 6 deletions

File tree

scripts/gdrive_app.py

Lines changed: 187 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

8385
class 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

100113
class 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

117131
class 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

145169
class 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

465489
class 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

513657
class 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

Comments
 (0)