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

View File

@@ -34,55 +34,34 @@ Ensure the following system dependencies are installed:
--- ---
## Quick Setup & Installation ## Quick Start (3-Step Setup)
### 1. Clone the Repository Setting up f0ckm Uploader takes less than a minute:
### 1. Run the Automated Installer
```bash ```bash
git clone https://git.lat/kibi/f0ckm-uploader git clone https://git.lat/kibi/f0ckm-uploader
cd f0ckm-uploader cd f0ckm-uploader
```
### 2. Install Python Dependencies
```bash
pip install PySide6
```
_(Or install via system package manager: `sudo dnf install python3-pyside6` / `sudo apt install python3-pyside6`)_
### 3. Environment Configuration _(Optional)_
You can set up a `.env` file prior to installation, or configure settings later via the GUI:
```bash
cp .env.example .env
```
Edit `.env`:
```env
API_URL=https://your-f0ckm-instance.com/api/v2/upload
API_KEY=your_secret_api_key
DEFAULT_RATING=s
DEFAULT_TAGS=
DEFAULT_VISIBILITY=0
DEFAULT_IS_OC=false
```
### 4. Run the Installer
```bash
chmod +x install.sh
./install.sh ./install.sh
``` ```
The installer will automatically: ### 2. Enter Your API Credentials
1. Copy binaries to `~/.local/bin/f0ckm-uploader-gui` and `~/.local/bin/f0ckm-uploader`. When `./install.sh` finishes, the **f0ckm Uploader Settings** GUI window will open automatically.
2. Generate multi-resolution icons (`32x32`, `48x48`, `64x64`, `128x128`, `256x256`) for KRunner & Start Menu compatibility. - Fill in your **API URL** (e.g. `https://your-f0ckm-site.com/api/v2/upload`)
3. Register the Dolphin context menu action in `~/.local/share/kio/servicemenus/`. - Paste your **API Key**
4. Register the `.desktop` application launchers and rebuild the KDE Plasma cache (`kbuildsycoca6`). - Click **Test Connection** -> **Save Settings**
### 3. Start Uploading!
- **System Tray**: Click the tray icon to take a screenshot and upload instantly.
- **Dolphin**: Right-click any file -> **Actions** -> **Upload to f0ckm**.
- **Browser**: Install the extension in `browser-extension/` to right-click web media and upload directly.
---
The installer handles dependency checking (PySide6), desktop entry creation, Dolphin context menus, multi-resolution application icons, and system tray startup automatically.
--- ---

View File

@@ -9,4 +9,4 @@ X-KDE-Submenu=Upload
[Desktop Action upload_custom_api] [Desktop Action upload_custom_api]
Name=Upload to f0ckm Name=Upload to f0ckm
Icon=cloud-upload-symbolic Icon=cloud-upload-symbolic
Exec=__EXEC_PATH__ "%f" Exec=__EXEC_PATH__ %F

View File

@@ -2,7 +2,7 @@
Type=Application Type=Application
Name=f0ckm Uploader (Internal) Name=f0ckm Uploader (Internal)
Comment=Upload to f0ckm (Backend registration) Comment=Upload to f0ckm (Backend registration)
Exec=__EXEC_PATH__ Exec=__EXEC_PATH__ %F
Icon=f0ckm-uploader Icon=f0ckm-uploader
Terminal=false Terminal=false
Categories=Utility;Network; Categories=Utility;Network;
@@ -12,5 +12,5 @@ InitialPreference=0
[Desktop Action upload] [Desktop Action upload]
Name=Upload to Custom API Name=Upload to Custom API
Exec=__EXEC_PATH__ Exec=__EXEC_PATH__ %F
Icon=f0ckm-uploader Icon=f0ckm-uploader

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

View File

@@ -6,6 +6,38 @@ APP_DIR="$HOME/.local/share/applications"
SERVICE_MENU_DIR="$HOME/.local/share/kio/servicemenus" SERVICE_MENU_DIR="$HOME/.local/share/kio/servicemenus"
LEGACY_SERVICE_MENU_DIR="$HOME/.local/share/kservices5/ServiceMenus" LEGACY_SERVICE_MENU_DIR="$HOME/.local/share/kservices5/ServiceMenus"
# Check Python 3 and PySide6 dependency
echo "Checking dependencies..."
if ! command -v python3 &>/dev/null; then
echo "Error: Python 3 is required but not found on your system."
exit 1
fi
if ! python3 -c "import PySide6" &>/dev/null; then
echo "PySide6 is not installed. Attempting automatic installation..."
if command -v pip3 &>/dev/null; then
pip3 install PySide6 || pip3 install --break-system-packages PySide6 || true
elif command -v pip &>/dev/null; then
pip install PySide6 || pip install --break-system-packages PySide6 || true
elif command -v dnf &>/dev/null; then
echo "Installing python3-pyside6 via dnf..."
sudo dnf install -y python3-pyside6 || true
elif command -v apt-get &>/dev/null; then
echo "Installing python3-pyside6 via apt..."
sudo apt-get update && sudo apt-get install -y python3-pyside6 || true
elif command -v pacman &>/dev/null; then
echo "Installing python-pyside6 via pacman..."
sudo pacman -S --noconfirm python-pyside6 || true
elif command -v zypper &>/dev/null; then
echo "Installing python3-pyside6 via zypper..."
sudo zypper install -y python3-pyside6 || true
fi
fi
if ! python3 -c "import PySide6" &>/dev/null; then
echo "Warning: Could not automatically install PySide6. Please install 'PySide6' via pip or your distro package manager (e.g. python3-pyside6)."
fi
# Create necessary directories # Create necessary directories
mkdir -p "$INSTALL_DIR" "$APP_DIR" "$SERVICE_MENU_DIR" "$LEGACY_SERVICE_MENU_DIR" mkdir -p "$INSTALL_DIR" "$APP_DIR" "$SERVICE_MENU_DIR" "$LEGACY_SERVICE_MENU_DIR"
@@ -154,9 +186,26 @@ except Exception as e:
print(f"Warning: could not sync autostart entry: {e}") print(f"Warning: could not sync autostart entry: {e}")
' '
echo "Installation complete!" echo ""
echo "The script has been installed to $INSTALL_DIR/f0ckm-uploader" echo "========================================================"
echo "The GUI has been installed to $INSTALL_DIR/f0ckm-uploader-gui" echo " f0ckm Uploader Installation Complete!"
echo "You can launch the GUI uploader from your applications menu (f0ckm Uploader GUI)." echo "========================================================"
echo "You can now right click files in Dolphin and find 'Upload to f0ckm' in the Actions menu." echo " -> Executable installed: $INSTALL_DIR/f0ckm-uploader-gui"
echo "In Spectacle, you should find 'Upload to f0ckm' in the Export or Share menu." echo " -> Dolphin context menu registered (Right-click file -> Actions -> Upload to f0ckm)"
echo " -> Spectacle screenshot export integration enabled"
echo " -> Browser extension helper API ready on http://127.0.0.1:18739"
echo "========================================================"
echo ""
# Launch GUI to prompt for API settings if API key or URL is not configured yet
python3 -c '
import json, os
cfg_path = os.path.expanduser("~/.config/f0ckm-uploader/config.json")
if os.path.exists(cfg_path):
with open(cfg_path) as f:
cfg = json.load(f)
if not cfg.get("api_url") or not cfg.get("api_key"):
print("[!] API Key or URL not configured yet.")
print(" Launching f0ckm Uploader GUI Settings for initial setup...")
os.system("'$INSTALL_DIR'/f0ckm-uploader-gui &")
' || true

View File

@@ -1,3 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Pass execution directly to the Python GUI engine # Pass execution directly to the Python GUI engine
exec f0ckm-uploader-gui --upload="$1" exec f0ckm-uploader-gui --upload "$@"