add queue functionality to upload many items and update readme to include basic installation guide
This commit is contained in:
59
README.md
59
README.md
@@ -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
|
||||
git clone https://git.lat/kibi/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
|
||||
```
|
||||
|
||||
The installer will automatically:
|
||||
### 2. Enter Your API Credentials
|
||||
|
||||
1. Copy binaries to `~/.local/bin/f0ckm-uploader-gui` and `~/.local/bin/f0ckm-uploader`.
|
||||
2. Generate multi-resolution icons (`32x32`, `48x48`, `64x64`, `128x128`, `256x256`) for KRunner & Start Menu compatibility.
|
||||
3. Register the Dolphin context menu action in `~/.local/share/kio/servicemenus/`.
|
||||
4. Register the `.desktop` application launchers and rebuild the KDE Plasma cache (`kbuildsycoca6`).
|
||||
When `./install.sh` finishes, the **f0ckm Uploader Settings** GUI window will open automatically.
|
||||
- Fill in your **API URL** (e.g. `https://your-f0ckm-site.com/api/v2/upload`)
|
||||
- Paste your **API Key**
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,4 +9,4 @@ X-KDE-Submenu=Upload
|
||||
[Desktop Action upload_custom_api]
|
||||
Name=Upload to f0ckm
|
||||
Icon=cloud-upload-symbolic
|
||||
Exec=__EXEC_PATH__ "%f"
|
||||
Exec=__EXEC_PATH__ %F
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Type=Application
|
||||
Name=f0ckm Uploader (Internal)
|
||||
Comment=Upload to f0ckm (Backend registration)
|
||||
Exec=__EXEC_PATH__
|
||||
Exec=__EXEC_PATH__ %F
|
||||
Icon=f0ckm-uploader
|
||||
Terminal=false
|
||||
Categories=Utility;Network;
|
||||
@@ -12,5 +12,5 @@ InitialPreference=0
|
||||
|
||||
[Desktop Action upload]
|
||||
Name=Upload to Custom API
|
||||
Exec=__EXEC_PATH__
|
||||
Exec=__EXEC_PATH__ %F
|
||||
Icon=f0ckm-uploader
|
||||
|
||||
317
gui.py
317
gui.py
@@ -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()
|
||||
|
||||
|
||||
61
install.sh
61
install.sh
@@ -6,6 +6,38 @@ APP_DIR="$HOME/.local/share/applications"
|
||||
SERVICE_MENU_DIR="$HOME/.local/share/kio/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
|
||||
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}")
|
||||
'
|
||||
|
||||
echo "Installation complete!"
|
||||
echo "The script has been installed to $INSTALL_DIR/f0ckm-uploader"
|
||||
echo "The GUI has been installed to $INSTALL_DIR/f0ckm-uploader-gui"
|
||||
echo "You can launch the GUI uploader from your applications menu (f0ckm Uploader GUI)."
|
||||
echo "You can now right click files in Dolphin and find 'Upload to f0ckm' in the Actions menu."
|
||||
echo "In Spectacle, you should find 'Upload to f0ckm' in the Export or Share menu."
|
||||
echo ""
|
||||
echo "========================================================"
|
||||
echo " f0ckm Uploader Installation Complete!"
|
||||
echo "========================================================"
|
||||
echo " -> Executable installed: $INSTALL_DIR/f0ckm-uploader-gui"
|
||||
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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pass execution directly to the Python GUI engine
|
||||
exec f0ckm-uploader-gui --upload="$1"
|
||||
exec f0ckm-uploader-gui --upload "$@"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user