add queue functionality to upload many items and update readme to include basic installation guide

This commit is contained in:
2026-08-12 17:14:59 +02:00
parent 35ff9bae5e
commit 43e95c9d3e
6 changed files with 277 additions and 169 deletions

317
gui.py
View File

@@ -73,6 +73,34 @@ def parse_upload_target(arg: str) -> tuple[str, bool]:
print(f"[f0ckm-gui] [PARSE] File path parsed: '{arg}' -> norm='{norm}' (is_url=False)", flush=True)
return (norm, False)
def parse_cli_args(args: list[str]) -> tuple[str, list[str]]:
if not args:
return ("show", [])
targets = []
action = "show"
for arg in args:
arg_str = arg.strip()
if not arg_str:
continue
if arg_str in ("--spectacle", "-s"):
action = "spectacle"
elif arg_str.startswith("--upload="):
action = "upload"
t, _ = parse_upload_target(arg_str[9:])
if t:
targets.append(t)
elif arg_str == "--upload":
action = "upload"
else:
t, _ = parse_upload_target(arg_str)
if t:
action = "upload"
targets.append(t)
return (action, targets)
class LocalHTTPBridge(QObject):
request_upload = Signal(str)
@@ -1417,6 +1445,14 @@ class SystemTrayApp(QObject):
self.network_manager = QNetworkAccessManager(self)
self.current_reply = None
# Queue system state
self.upload_queue = [] # List of tuples: (target_str, is_url)
self.is_uploading = False
self.active_thread = None
self.current_target = None
self.total_queue_count = 0
self.current_item_index = 0
self.menu = QMenu()
# Header entry
@@ -1469,6 +1505,63 @@ class SystemTrayApp(QObject):
self.settings_dialog = None
self.gallery_window = None
def enqueue_targets(self, targets):
if isinstance(targets, str):
targets = [targets]
valid_targets = []
for t in targets:
target_str, is_url = parse_upload_target(t)
if target_str:
valid_targets.append((target_str, is_url))
if not valid_targets:
return
if not self.is_uploading and len(self.upload_queue) == 0:
self.total_queue_count = len(valid_targets)
self.current_item_index = 0
self.upload_queue.extend(valid_targets)
self.process_next_in_queue()
else:
self.upload_queue.extend(valid_targets)
self.total_queue_count += len(valid_targets)
print(f"[f0ckm-gui] [QUEUE] Enqueued {len(valid_targets)} items. Total in queue: {len(self.upload_queue)} (Batch total: {self.total_queue_count})", flush=True)
def process_next_in_queue(self):
if len(self.upload_queue) == 0:
self.is_uploading = False
self.current_target = None
self.active_thread = None
self.total_queue_count = 0
self.current_item_index = 0
self.abort_action.setEnabled(False)
self.tray.setIcon(self.default_icon)
self.tray.setToolTip("f0ckm Uploader (Click to capture & upload)")
return
target_tuple = self.upload_queue.pop(0)
target_str, is_url = target_tuple
self.is_uploading = True
self.current_target = target_str
self.current_item_index += 1
self.abort_action.setEnabled(True)
print(f"[f0ckm-gui] [QUEUE ITEM {self.current_item_index}/{self.total_queue_count}] Processing target: '{target_str}' (is_url={is_url})", flush=True)
if is_url:
self.download_and_upload_url(target_str)
else:
self.upload_file_direct(target_str)
def _on_single_task_complete(self):
if self.active_thread:
try:
self.active_thread.deleteLater()
except Exception:
pass
self.active_thread = None
QTimer.singleShot(50, self.process_next_in_queue)
def on_notification_clicked(self):
url = getattr(self, "last_uploaded_url", "")
if url:
@@ -1527,36 +1620,34 @@ class SystemTrayApp(QObject):
def on_capture_and_upload(self):
screenshot_dir = os.path.expanduser("~/Pictures/Screenshots")
os.makedirs(screenshot_dir, exist_ok=True)
save_path = os.path.join(screenshot_dir, f"screenshot_{time.strftime('%Y%m%d_%H%M%S')}.png")
ts = time.strftime('%Y%m%d_%H%M%S')
ms = int(time.time() * 1000) % 1000
save_path = os.path.join(screenshot_dir, f"screenshot_{ts}_{ms:03d}.png")
try:
self.spectacle_proc = QProcess(self)
self.spectacle_proc.finished.connect(lambda exit_code, exit_status: self._on_spectacle_finished(save_path, exit_code))
self.spectacle_proc.start("spectacle", ["-r", "-b", "-n", "-o", save_path])
proc = QProcess(self)
proc.finished.connect(lambda exit_code, exit_status, p=proc, sp=save_path: self._on_spectacle_finished(p, sp, exit_code))
proc.start("spectacle", ["-r", "-b", "-n", "-o", save_path])
except Exception as e:
self.show_message("Upload Error", f"Failed to capture screenshot: {e}", QSystemTrayIcon.Critical)
def _on_spectacle_finished(self, save_path, exit_code):
def _on_spectacle_finished(self, proc, save_path, exit_code):
proc.deleteLater()
if os.path.exists(save_path) and os.path.getsize(save_path) > 0:
self.upload_file_direct(save_path)
self.enqueue_targets([save_path])
def on_abort_upload(self):
if hasattr(self, "download_thread") and self.download_thread and self.download_thread.isRunning():
self.download_thread.abort()
print(f"[f0ckm-gui] [QUEUE] Abort requested. Clearing queue ({len(self.upload_queue)} remaining items) and stopping active upload.", flush=True)
self.upload_queue.clear()
self.total_queue_count = 0
self.current_item_index = 0
if self.active_thread and hasattr(self.active_thread, "abort"):
self.active_thread.abort()
if self.current_reply and self.current_reply.isRunning():
self.current_reply.abort()
elif hasattr(self, "upload_thread") and self.upload_thread and self.upload_thread.isRunning():
self.upload_thread.abort()
def upload_target_direct(self, target):
target_str, is_url = parse_upload_target(target)
print(f"[f0ckm-gui] [TRAY ACTION] Received target: '{target}' -> target_str='{target_str}', is_url={is_url}", flush=True)
if not target_str:
return
if is_url:
self.download_and_upload_url(target_str)
else:
self.upload_file_direct(target_str)
self.enqueue_targets([target])
def download_and_upload_url(self, url):
config = get_env_config()
@@ -1571,17 +1662,20 @@ class SystemTrayApp(QObject):
if not api_url or not api_key:
print(f"[f0ckm-gui] [CONFIG ERROR] API URL or API Key missing in configuration!", flush=True)
self.show_message("Upload Error", "API URL and API Key must be set in Settings.", QSystemTrayIcon.Critical)
QTimer.singleShot(100, self._on_single_task_complete)
return
self.download_url = url
self.download_start_time = time.time()
self.tray.setIcon(create_digit_icon(50))
self.tray.setToolTip(f"Uploading URL to f0ckm...\n{url[:45]}")
self.url_post_thread = URLPostThread(api_url, api_key, url, rating, tags, visibility, is_oc)
self.url_post_thread.finished.connect(lambda ok, res: self.on_url_post_finished(ok, res, url))
self.url_post_thread.start()
self.abort_action.setEnabled(True)
batch_prefix = f"[{self.current_item_index}/{self.total_queue_count}] " if self.total_queue_count > 1 else ""
rem_str = f"\n({len(self.upload_queue)} waiting in queue)" if self.upload_queue else ""
self.tray.setToolTip(f"Uploading URL {batch_prefix}to f0ckm...\n{url[:45]}{rem_str}")
self.active_thread = URLPostThread(api_url, api_key, url, rating, tags, visibility, is_oc)
self.active_thread.finished.connect(lambda ok, res: self.on_url_post_finished(ok, res, url))
self.active_thread.start()
def on_url_post_finished(self, ok, result, url):
if ok:
@@ -1608,22 +1702,22 @@ class SystemTrayApp(QObject):
}
add_history_entry(entry)
self.abort_action.setEnabled(False)
self.tray.setIcon(self.default_icon)
self.tray.setToolTip("f0ckm Uploader (Click to capture & upload)")
self.last_uploaded_url = final_url
self.show_success_notification(url, final_url)
self._on_single_task_complete()
return
except Exception as e:
print(f"[f0ckm-gui] JSON parse notice: {e}", flush=True)
print(f"[f0ckm-gui] Direct URL API notice: Falling back to local download of '{url}'", flush=True)
self.download_thread = StreamingDownloadThread(url)
self.download_thread.progress.connect(self.on_download_progress)
self.download_thread.finished.connect(lambda ok_dl, res_dl: self.on_download_finished(ok_dl, res_dl, url))
self.download_thread.start()
self.active_thread = StreamingDownloadThread(url)
self.active_thread.progress.connect(self.on_download_progress)
self.active_thread.finished.connect(lambda ok_dl, res_dl: self.on_download_finished(ok_dl, res_dl, url))
self.active_thread.start()
def on_download_progress(self, bytes_received, bytes_total):
batch_prefix = f"[{self.current_item_index}/{self.total_queue_count}] " if self.total_queue_count > 1 else ""
rem_str = f"\n({len(self.upload_queue)} waiting in queue)" if self.upload_queue else ""
if bytes_total > 0:
pct = min(99, max(0, int((bytes_received / bytes_total) * 100)))
self.tray.setIcon(create_digit_icon(pct))
@@ -1631,20 +1725,18 @@ class SystemTrayApp(QObject):
tot_str = format_size(bytes_total)
url_str = getattr(self, "download_url", "")
short_url = (url_str[:35] + "...") if len(url_str) > 35 else url_str
self.tray.setToolTip(f"Downloading: {short_url}\n{pct}% ({rec_str} / {tot_str})")
self.tray.setToolTip(f"Downloading {batch_prefix}: {short_url}\n{pct}% ({rec_str} / {tot_str}){rem_str}")
else:
rec_str = format_size(bytes_received)
self.tray.setToolTip(f"Downloading remote file...\n{rec_str}")
self.tray.setToolTip(f"Downloading remote file {batch_prefix}...\n{rec_str}{rem_str}")
def on_download_finished(self, success, result, url):
if not success:
self.abort_action.setEnabled(False)
self.tray.setIcon(self.default_icon)
self.tray.setToolTip("f0ckm Uploader (Click to capture & upload)")
if result == "ABORTED":
self.show_message("Download Aborted", "Remote URL download was canceled.", QSystemTrayIcon.Information)
else:
self.show_message("Download Failed", f"Failed to download remote URL: {result}", QSystemTrayIcon.Critical)
self._on_single_task_complete()
return
self.upload_file_direct(result)
@@ -1653,6 +1745,7 @@ class SystemTrayApp(QObject):
file_path = normalize_file_path(file_path)
if not file_path or not os.path.exists(file_path):
self.show_message("Upload Error", f"File not found: {file_path}", QSystemTrayIcon.Critical)
QTimer.singleShot(100, self._on_single_task_complete)
return
config = get_env_config()
@@ -1660,6 +1753,7 @@ class SystemTrayApp(QObject):
api_key = config.get("api_key", "")
if not api_url or not api_key:
self.show_message("Upload Error", "API URL and API Key must be set in Settings.", QSystemTrayIcon.Critical)
QTimer.singleShot(100, self._on_single_task_complete)
return
rating = config.get("default_rating", "")
@@ -1676,29 +1770,15 @@ class SystemTrayApp(QObject):
self.upload_start_time = time.time()
self.tray.setIcon(create_digit_icon(0))
self.tray.setToolTip(f"Uploading {filename}...\n0% (0 B)")
self.upload_thread = StreamingUploadThread(api_url, api_key, file_path, rating, tags, visibility, is_oc)
self.upload_thread.progress.connect(self.on_upload_progress)
self.upload_thread.finished.connect(lambda ok, resp: self.on_upload_finished(ok, resp, file_path))
self.upload_thread.start()
batch_prefix = f"[{self.current_item_index}/{self.total_queue_count}] " if self.total_queue_count > 1 else ""
rem_str = f"\n({len(self.upload_queue)} waiting in queue)" if self.upload_queue else ""
self.tray.setToolTip(f"Uploading {batch_prefix}{filename}...\n0% (0 B){rem_str}")
self.abort_action.setEnabled(True)
def _on_qt_reply_finished(self, reply, file_path):
self.current_reply = None
err = reply.error()
if err == QNetworkReply.NetworkError.NoError:
resp_bytes = reply.readAll().data()
resp_str = resp_bytes.decode('utf-8', errors='replace')
self.on_upload_finished(True, resp_str, file_path)
else:
if err == QNetworkReply.NetworkError.OperationCanceledError:
self.on_upload_finished(False, "ABORTED", file_path)
else:
resp_bytes = reply.readAll().data()
resp_str = resp_bytes.decode('utf-8', errors='replace') if resp_bytes else reply.errorString()
self.on_upload_finished(False, resp_str, file_path)
self.active_thread = StreamingUploadThread(api_url, api_key, file_path, rating, tags, visibility, is_oc)
self.active_thread.progress.connect(self.on_upload_progress)
self.active_thread.finished.connect(lambda ok, resp: self.on_upload_finished(ok, resp, file_path))
self.active_thread.start()
def on_upload_progress(self, bytes_sent, bytes_total):
if bytes_total > 0:
@@ -1712,8 +1792,12 @@ class SystemTrayApp(QObject):
total_str = format_size(bytes_total)
filename = getattr(self, "upload_filename", "file")
batch_prefix = f"[{self.current_item_index}/{self.total_queue_count}] " if self.total_queue_count > 1 else ""
remaining = len(self.upload_queue)
rem_str = f"\n({remaining} waiting in queue)" if remaining > 0 else ""
if pct >= 99:
tooltip = f"Uploading: {filename}\n99% ({total_str})\nProcessing upload on server..."
tooltip = f"Uploading {batch_prefix}: {filename}\n99% ({total_str})\nProcessing upload on server...{rem_str}"
else:
speed_str = format_speed(speed)
remaining_bytes = bytes_total - bytes_sent
@@ -1725,24 +1809,17 @@ class SystemTrayApp(QObject):
else:
eta_str = f"{eta_sec // 3600}h {(eta_sec % 3600) // 60}m"
tooltip = f"Uploading: {filename}\n{pct}% ({sent_str} / {total_str})\nSpeed: {speed_str} | ETA: {eta_str}"
tooltip = f"Uploading {batch_prefix}: {filename}\n{pct}% ({sent_str} / {total_str})\nSpeed: {speed_str} | ETA: {eta_str}{rem_str}"
self.tray.setToolTip(tooltip)
def on_abort_upload(self):
if hasattr(self, "upload_thread") and self.upload_thread:
self.upload_thread.abort()
def on_upload_finished(self, success, response_text, file_path):
self.abort_action.setEnabled(False)
self.tray.setIcon(self.default_icon)
self.tray.setToolTip("f0ckm Uploader (Click to capture & upload)")
if not success:
if response_text == "ABORTED":
self.show_message("Upload Aborted", "The active upload was canceled.", QSystemTrayIcon.Information)
else:
self.show_message("Upload Failed", response_text, QSystemTrayIcon.Critical)
self._on_single_task_complete()
return
try:
@@ -1779,6 +1856,8 @@ class SystemTrayApp(QObject):
self.show_message("Upload Failed", msg, QSystemTrayIcon.Critical)
except Exception as e:
self.show_message("Upload Error", f"Error parsing response: {e}\nResponse: {response_text}", QSystemTrayIcon.Critical)
finally:
self._on_single_task_complete()
def show_success_notification(self, file_path, item_url):
self.last_uploaded_url = item_url
@@ -1838,16 +1917,16 @@ class SystemTrayApp(QObject):
print(f"Notification error: {e}")
def on_upload_file(self):
file_path, _ = QFileDialog.getOpenFileName(
None, "Select File to Upload", "", "All Files (*)"
file_paths, _ = QFileDialog.getOpenFileNames(
None, "Select Files to Upload", "", "All Files (*)"
)
if file_path:
self.upload_file_direct(file_path)
if file_paths:
self.enqueue_targets(file_paths)
def on_upload_url(self):
url, ok = QInputDialog.getText(None, "Upload URL", "Enter image or file URL:")
if ok and url.strip():
self.upload_target_direct(url.strip())
self.enqueue_targets([url.strip()])
def on_upload_clipboard(self):
clipboard = QApplication.clipboard()
@@ -1856,36 +1935,41 @@ class SystemTrayApp(QObject):
if mime_data.hasImage():
image = clipboard.image()
temp_dir = tempfile.gettempdir()
temp_path = os.path.join(temp_dir, f"f0ckm_clipboard_{int(time.time())}.png")
ts = time.strftime('%Y%m%d_%H%M%S')
ms = int(time.time() * 1000) % 1000
temp_path = os.path.join(temp_dir, f"f0ckm_clipboard_{ts}_{ms:03d}.png")
if image.save(temp_path, "PNG"):
self.upload_file_direct(temp_path)
self.enqueue_targets([temp_path])
else:
self.show_message("Upload Error", "Failed to save clipboard image to temporary file.", QSystemTrayIcon.Critical)
elif mime_data.hasUrls():
urls = mime_data.urls()
uploaded_any = False
valid_paths = []
for url in urls:
if url.isLocalFile():
path = url.toLocalFile()
if os.path.exists(path):
self.upload_file_direct(path)
uploaded_any = True
if not uploaded_any:
valid_paths.append(path)
if valid_paths:
self.enqueue_targets(valid_paths)
else:
self.show_message("Upload Error", "No valid local files in clipboard.", QSystemTrayIcon.Warning)
elif mime_data.hasText():
text = mime_data.text().strip()
lines = [l.strip() for l in text.split('\n') if l.strip()]
uploaded_any = False
valid_targets = []
for line in lines:
if line.startswith("file://"):
line = line[7:]
if os.path.exists(line):
self.upload_file_direct(line)
uploaded_any = True
if not uploaded_any:
self.show_message("Upload Error", "Clipboard text is not a valid local file path.", QSystemTrayIcon.Warning)
t, _ = parse_upload_target(line)
if t:
valid_targets.append(t)
if valid_targets:
self.enqueue_targets(valid_targets)
else:
self.show_message("Upload Error", "Clipboard text does not contain valid file paths or URLs.", QSystemTrayIcon.Warning)
else:
self.show_message("Upload Error", "Clipboard does not contain an image or file paths.", QSystemTrayIcon.Warning)
self.show_message("Upload Error", "Clipboard does not contain an image, files, or URLs.", QSystemTrayIcon.Warning)
def show_message(self, title, message, icon=QSystemTrayIcon.Information):
self.tray.showMessage(title, message, icon, 5000)
@@ -1903,26 +1987,10 @@ class SingleInstanceApp:
socket = QLocalSocket()
socket.connectToServer(self.name)
if socket.waitForConnected(500):
msg = "show"
if len(sys.argv) > 1:
arg = sys.argv[1]
if arg in ("--spectacle", "-s"):
msg = "spectacle"
elif arg.startswith("--upload="):
target, _ = parse_upload_target(arg[9:])
msg = f"upload:{target}"
elif arg.startswith("--upload"):
if len(sys.argv) > 2:
target, _ = parse_upload_target(sys.argv[2])
msg = f"upload:{target}"
else:
msg = "show"
else:
target, _ = parse_upload_target(arg)
if target:
msg = f"upload:{target}"
print(f"[f0ckm-gui] [IPC CLIENT] Daemon running. Forwarding message via socket: '{msg}'", flush=True)
socket.write(msg.encode("utf-8"))
action, targets = parse_cli_args(sys.argv[1:])
payload = json.dumps({"action": action, "targets": targets})
print(f"[f0ckm-gui] [IPC CLIENT] Daemon running. Forwarding payload via socket: '{payload}'", flush=True)
socket.write(payload.encode("utf-8"))
socket.waitForBytesWritten(500)
socket.disconnectFromServer()
return False
@@ -1964,11 +2032,26 @@ def main():
def on_activate(msg_str):
nonlocal tray_app
if tray_app:
if msg_str.startswith("{"):
try:
data = json.loads(msg_str)
act = data.get("action", "show")
targets = data.get("targets", [])
if act == "spectacle":
tray_app.on_capture_and_upload()
elif act == "upload" and targets:
tray_app.enqueue_targets(targets)
else:
tray_app.on_open_settings()
return
except Exception as e:
print(f"[f0ckm-gui] IPC JSON parse notice: {e}", flush=True)
if msg_str == "spectacle":
tray_app.on_capture_and_upload()
elif msg_str.startswith("upload:"):
target = msg_str[7:]
tray_app.upload_target_direct(target)
tray_app.enqueue_targets([target])
else:
tray_app.on_open_settings()
@@ -1982,28 +2065,24 @@ def main():
set_autostart(cfg.get("autostart", False))
tray_app = SystemTrayApp()
local_http_bridge.request_upload.connect(tray_app.upload_target_direct)
local_http_bridge.request_upload.connect(lambda url: tray_app.enqueue_targets([url]))
tray_app.show()
start_local_http_server(tray_app)
# Check if started with action argument
if len(sys.argv) > 1:
arg = sys.argv[1]
if arg in ("--spectacle", "-s"):
action, targets = parse_cli_args(sys.argv[1:])
if action == "spectacle":
tray_app.on_capture_and_upload()
elif arg.startswith("--upload="):
target, _ = parse_upload_target(arg[9:])
tray_app.upload_target_direct(target)
elif arg.startswith("--upload"):
if len(sys.argv) > 2:
target, _ = parse_upload_target(sys.argv[2])
tray_app.upload_target_direct(target)
else:
target, _ = parse_upload_target(arg)
if target:
tray_app.upload_target_direct(target)
elif action == "upload" and targets:
tray_app.enqueue_targets(targets)
else:
if not cfg.get("api_url") or not cfg.get("api_key"):
print("[f0ckm-gui] First launch detected (missing API credentials). Opening settings dialog...", flush=True)
QTimer.singleShot(400, tray_app.on_open_settings)
sys.exit(app.exec())
if __name__ == "__main__":
main()