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
|
```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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
315
gui.py
315
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)
|
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:
|
else:
|
||||||
self.show_message("Upload Error", "Clipboard does not contain an image or file paths.", QSystemTrayIcon.Warning)
|
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, 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)
|
|
||||||
elif arg.startswith("--upload"):
|
|
||||||
if len(sys.argv) > 2:
|
|
||||||
target, _ = parse_upload_target(sys.argv[2])
|
|
||||||
tray_app.upload_target_direct(target)
|
|
||||||
else:
|
else:
|
||||||
target, _ = parse_upload_target(arg)
|
if not cfg.get("api_url") or not cfg.get("api_key"):
|
||||||
if target:
|
print("[f0ckm-gui] First launch detected (missing API credentials). Opening settings dialog...", flush=True)
|
||||||
tray_app.upload_target_direct(target)
|
QTimer.singleShot(400, tray_app.on_open_settings)
|
||||||
|
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
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"
|
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
|
||||||
|
|||||||
@@ -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 "$@"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user