This commit is contained in:
2026-08-11 22:32:42 +02:00
parent b65ce306a2
commit e4d82e1c7e

383
gui.py
View File

@@ -10,6 +10,7 @@ import time
import mimetypes
import urllib.parse
import re
import shutil
def normalize_file_path(path_str: str) -> str:
if not path_str:
@@ -28,7 +29,7 @@ from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QHttpMulti
from PySide6.QtWidgets import (
QApplication, QSystemTrayIcon, QMenu, QDialog, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton, QGroupBox,
QFormLayout, QMessageBox, QFileDialog
QFormLayout, QMessageBox, QFileDialog, QSlider
)
# Configuration paths
@@ -215,7 +216,7 @@ class SettingsDialog(QDialog):
super().__init__(parent)
self.tray_app = tray_app
self.setWindowTitle("f0ckm Uploader Settings")
self.resize(520, 560)
self.resize(540, 680)
app_icon = get_app_icon()
if not app_icon.isNull():
@@ -331,6 +332,45 @@ class SettingsDialog(QDialog):
main_layout.addWidget(grp_behavior)
# Section 4: Sound Notification
grp_sound = QGroupBox("Audio Notification")
sound_layout = QVBoxLayout(grp_sound)
sound_layout.setContentsMargins(15, 20, 15, 15)
sound_layout.setSpacing(10)
self.chk_sound = QCheckBox("Play sound on successful upload")
sound_layout.addWidget(self.chk_sound)
file_picker_layout = QHBoxLayout()
self.txt_sound_path = QLineEdit()
self.txt_sound_path.setPlaceholderText("Select native audio file (.wav, .ogg, .flac)...")
file_picker_layout.addWidget(self.txt_sound_path)
self.btn_browse_sound = QPushButton("Browse...")
self.btn_browse_sound.clicked.connect(self.on_browse_sound)
file_picker_layout.addWidget(self.btn_browse_sound)
self.btn_test_sound = QPushButton("Test Sound")
self.btn_test_sound.clicked.connect(self.on_test_sound)
file_picker_layout.addWidget(self.btn_test_sound)
sound_layout.addLayout(file_picker_layout)
vol_layout = QHBoxLayout()
vol_label = QLabel("Volume:")
self.slider_volume = QSlider(Qt.Orientation.Horizontal)
self.slider_volume.setRange(0, 100)
self.slider_volume.setValue(15)
self.lbl_volume = QLabel("15%")
self.lbl_volume.setFixedWidth(40)
self.slider_volume.valueChanged.connect(lambda v: self.lbl_volume.setText(f"{v}%"))
vol_layout.addWidget(vol_label)
vol_layout.addWidget(self.slider_volume)
vol_layout.addWidget(self.lbl_volume)
sound_layout.addLayout(vol_layout)
main_layout.addWidget(grp_sound)
# Save / Cancel Buttons
btn_layout = QHBoxLayout()
btn_layout.addStretch()
@@ -378,6 +418,18 @@ class SettingsDialog(QDialog):
icon_theme = config.get("icon_theme", "dark").lower()
self.cb_icon_theme.setCurrentIndex(1 if icon_theme == "light" else 0)
self.chk_sound.setChecked(config.get("play_success_sound", False))
self.txt_sound_path.setText(config.get("success_audio_path", ""))
vol = config.get("success_audio_volume", 15)
try:
vol = int(vol)
except Exception:
vol = 15
vol = min(100, max(0, vol))
self.slider_volume.setValue(vol)
self.lbl_volume.setText(f"{vol}%")
def on_test_connection(self):
self.btn_test.setEnabled(False)
self.btn_test.setText("Testing...")
@@ -406,6 +458,32 @@ class SettingsDialog(QDialog):
else:
QMessageBox.warning(self, "Connection Test Failed", message)
def on_browse_sound(self):
file_path, _ = QFileDialog.getOpenFileName(
self,
"Select Native Audio File for Success Notification",
"",
"Supported Audio Files (*.wav *.ogg *.oga *.flac *.aiff *.aif *.au);;WAV Audio (*.wav);;OGG Audio (*.ogg *.oga);;FLAC Audio (*.flac)"
)
if file_path:
self.txt_sound_path.setText(file_path)
def on_test_sound(self):
sound_path = self.txt_sound_path.text().strip()
if not sound_path or not os.path.exists(sound_path):
QMessageBox.warning(self, "Invalid File", "Please select a valid audio file first.")
return
ext = os.path.splitext(sound_path)[1].lower()
supported = [".wav", ".ogg", ".oga", ".flac", ".aiff", ".aif", ".au"]
if ext not in supported:
QMessageBox.warning(
self,
"Unsupported Format",
f"The format '{ext}' is not supported natively by PipeWire/PulseAudio.\nPlease select a .wav, .ogg, or .flac file."
)
return
play_success_sound(sound_path, volume=self.slider_volume.value())
def save_settings(self):
config = {
"api_url": self.txt_url.text().strip(),
@@ -417,7 +495,10 @@ class SettingsDialog(QDialog):
"enable_notifications": self.chk_notifications.isChecked(),
"show_progress_dialog": self.chk_progress.isChecked(),
"autostart": self.chk_autostart.isChecked(),
"icon_theme": "light" if self.cb_icon_theme.currentIndex() == 1 else "dark"
"icon_theme": "light" if self.cb_icon_theme.currentIndex() == 1 else "dark",
"play_success_sound": self.chk_sound.isChecked(),
"success_audio_path": self.txt_sound_path.text().strip(),
"success_audio_volume": self.slider_volume.value()
}
if save_config(config):
@@ -519,6 +600,190 @@ def generate_thumbnail(file_path: str) -> str:
return ""
def format_size(bytes_val: int) -> str:
if bytes_val < 1024:
return f"{bytes_val} B"
elif bytes_val < 1024 * 1024:
return f"{bytes_val / 1024:.1f} KB"
elif bytes_val < 1024 * 1024 * 1024:
return f"{bytes_val / (1024 * 1024):.1f} MB"
else:
return f"{bytes_val / (1024 * 1024 * 1024):.2f} GB"
def format_speed(bytes_per_sec: float) -> str:
if bytes_per_sec < 1024:
return f"{int(bytes_per_sec)} B/s"
elif bytes_per_sec < 1024 * 1024:
return f"{bytes_per_sec / 1024:.1f} KB/s"
else:
return f"{bytes_per_sec / (1024 * 1024):.1f} MB/s"
def play_success_sound(audio_path=None, volume=None):
config = get_env_config()
if not audio_path:
audio_path = config.get("success_audio_path", "")
if not audio_path or not os.path.exists(audio_path):
return
if volume is None:
volume = config.get("success_audio_volume", 15)
try:
volume = min(100, max(0, int(volume)))
except Exception:
volume = 15
ext = os.path.splitext(audio_path)[1].lower()
supported = [".wav", ".ogg", ".oga", ".flac", ".aiff", ".aif", ".au"]
if ext not in supported:
return
vol_float = volume / 100.0
pa_vol_int = int(65536 * vol_float)
# Use native Linux audio daemons (pw-play, paplay, canberra-gtk-play, aplay)
for player in ["pw-play", "paplay", "canberra-gtk-play", "aplay"]:
if shutil.which(player):
try:
if player == "pw-play":
cmd = [player, "--volume", f"{vol_float:.2f}", audio_path]
elif player == "paplay":
cmd = [player, f"--volume={pa_vol_int}", audio_path]
elif player == "canberra-gtk-play":
cmd = [player, "-f", audio_path]
else:
cmd = [player, audio_path]
subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
return
except Exception:
pass
# Fallback to Qt QSoundEffect for WAV files
if ext == ".wav":
try:
from PySide6.QtMultimedia import QSoundEffect
effect = QSoundEffect()
effect.setSource(QUrl.fromLocalFile(audio_path))
effect.setVolume(vol_float)
effect.play()
app = QApplication.instance()
if app:
if not hasattr(app, "_sound_effects"):
app._sound_effects = []
app._sound_effects.append(effect)
effect.playingChanged.connect(
lambda: app._sound_effects.remove(effect) if not effect.isPlaying() and effect in getattr(app, "_sound_effects", []) else None
)
return
except Exception:
pass
class CurlUploadThread(QThread):
progress = Signal(int, int) # bytes_sent, bytes_total
finished = Signal(bool, str) # success, response_or_error_msg
def __init__(self, api_url, api_key, file_path, rating, tags, visibility, is_oc, parent=None):
super().__init__(parent)
self.api_url = api_url
self.api_key = api_key
self.file_path = file_path
self.rating = rating
self.tags = tags
self.visibility = visibility
self.is_oc = is_oc
self.process = None
self.file_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0
self.aborted = False
def abort(self):
self.aborted = True
if self.process:
try:
self.process.kill()
except Exception:
pass
def run(self):
mime_type, _ = mimetypes.guess_type(self.file_path)
if not mime_type:
ext = os.path.splitext(self.file_path)[1].lower()
if ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']:
mime_type = f"image/{ext.lstrip('.')}"
if mime_type == "image/jpg":
mime_type = "image/jpeg"
elif ext in ['.mp4', '.webm', '.mkv', '.mov', '.avi']:
mime_type = f"video/{ext.lstrip('.')}"
else:
mime_type = "image/png"
file_param = f"file=@{self.file_path}"
if mime_type:
file_param += f";type={mime_type}"
cmd = [
"curl", "-#",
"-F", file_param
]
if self.api_key:
cmd.extend(["-H", f"X-Api-Key: {self.api_key}"])
if self.rating:
cmd.extend(["-F", f"rating={self.rating}"])
if self.tags:
cmd.extend(["-F", f"tags={self.tags}"])
if self.visibility != "":
cmd.extend(["-F", f"visibility={self.visibility}"])
if self.is_oc != "0":
cmd.extend(["-F", f"is_oc={self.is_oc}"])
cmd.append(self.api_url)
try:
self.process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1
)
buffer = ""
while True:
char = self.process.stderr.read(1)
if not char and self.process.poll() is not None:
break
if char in ("\r", "\n"):
matches = re.findall(r'(\d+(?:\.\d+)?)\s*%', buffer)
if matches:
try:
pct = float(matches[-1])
bytes_sent = int(self.file_size * (pct / 100.0))
self.progress.emit(bytes_sent, self.file_size)
except Exception:
pass
buffer = ""
else:
buffer += char
stdout_data, stderr_data = self.process.communicate()
exit_code = self.process.returncode
if self.aborted:
self.finished.emit(False, "ABORTED")
elif exit_code == 0:
self.finished.emit(True, stdout_data)
else:
self.finished.emit(False, stderr_data or f"curl exited with code {exit_code}")
except Exception as e:
if self.aborted:
self.finished.emit(False, "ABORTED")
else:
self.finished.emit(False, str(e))
# ==========================================
# System Tray Application Manager
# ==========================================
@@ -554,6 +819,11 @@ class SystemTrayApp(QObject):
clipboard_action.triggered.connect(self.on_upload_clipboard)
self.menu.addAction(clipboard_action)
self.abort_action = QAction("Abort Upload", self.menu)
self.abort_action.setEnabled(False)
self.abort_action.triggered.connect(self.on_abort_upload)
self.menu.addAction(self.abort_action)
settings_action = QAction("Settings...", self.menu)
settings_action.triggered.connect(self.on_open_settings)
self.menu.addAction(settings_action)
@@ -641,66 +911,67 @@ class SystemTrayApp(QObject):
visibility = str(config.get("default_visibility", "0"))
is_oc = "1" if config.get("default_is_oc", False) else "0"
self.tray.setIcon(create_digit_icon(0))
self.tray.setToolTip("Uploading: 0%")
request = QNetworkRequest(QUrl(api_url))
if api_key:
request.setRawHeader(b"X-Api-Key", api_key.encode("utf-8"))
multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
file_part = QHttpPart()
filename = os.path.basename(file_path)
file_part.setHeader(QNetworkRequest.ContentDispositionHeader, f'form-data; name="file"; filename="{filename}"')
self.upload_filename = filename
self.upload_start_time = time.time()
mime_type, _ = mimetypes.guess_type(file_path)
if not mime_type:
mime_type = "image/png" if file_path.lower().endswith(".png") else "application/octet-stream"
file_part.setHeader(QNetworkRequest.ContentTypeHeader, mime_type)
self.tray.setIcon(create_digit_icon(0))
self.tray.setToolTip(f"Uploading {filename}...\n0% (0 B)")
qfile = QFile(file_path)
if not qfile.open(QIODevice.ReadOnly):
self.tray.setIcon(self.default_icon)
self.show_message("Upload Error", f"Cannot open file: {file_path}", QSystemTrayIcon.Critical)
return
self.upload_thread = CurlUploadThread(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()
file_part.setBodyDevice(qfile)
qfile.setParent(multi_part)
multi_part.append(file_part)
for name, val in [("rating", rating), ("tags", tags), ("visibility", visibility), ("is_oc", is_oc)]:
if val != "":
p = QHttpPart()
p.setHeader(QNetworkRequest.ContentDispositionHeader, f'form-data; name="{name}"')
p.setBody(str(val).encode("utf-8"))
multi_part.append(p)
reply = self.network_manager.post(request, multi_part)
multi_part.setParent(reply)
reply.qfile = qfile
reply.multi_part = multi_part
self.current_reply = reply
reply.uploadProgress.connect(self.on_upload_progress)
reply.finished.connect(lambda: self.on_upload_finished(reply, file_path))
self.abort_action.setEnabled(True)
def on_upload_progress(self, bytes_sent, bytes_total):
if bytes_total > 0:
pct = min(100, max(0, int((bytes_sent / bytes_total) * 100)))
self.tray.setIcon(create_digit_icon(pct))
self.tray.setToolTip(f"Uploading: {pct}%")
def on_upload_finished(self, reply, file_path):
elapsed = time.time() - getattr(self, "upload_start_time", time.time())
speed = (bytes_sent / elapsed) if elapsed > 0.1 else 0
sent_str = format_size(bytes_sent)
total_str = format_size(bytes_total)
filename = getattr(self, "upload_filename", "file")
if pct >= 99:
tooltip = f"Uploading: {filename}\n99% ({total_str})\nProcessing upload on server..."
else:
speed_str = format_speed(speed)
remaining_bytes = bytes_total - bytes_sent
eta_sec = int(remaining_bytes / speed) if speed > 0 else 0
if eta_sec < 60:
eta_str = f"{eta_sec}s"
elif eta_sec < 3600:
eta_str = f"{eta_sec // 60}m {eta_sec % 60}s"
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}"
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)")
try:
err_body = reply.readAll().data().decode("utf-8", errors="ignore")
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)
return
if reply.error() == QNetworkReply.NoError:
try:
res = json.loads(err_body)
res = json.loads(response_text)
if res.get("success") and res.get("url"):
item_url = res.get("url")
QApplication.clipboard().setText(item_url)
@@ -709,18 +980,16 @@ class SystemTrayApp(QObject):
msg = res.get("msg", "Upload failed")
self.show_message("Upload Failed", msg, QSystemTrayIcon.Critical)
except Exception as e:
self.show_message("Upload Error", f"Error parsing response: {e}", QSystemTrayIcon.Critical)
else:
server_msg = err_body if err_body else reply.errorString()
self.show_message("Upload Failed", f"Server Response: {server_msg}", QSystemTrayIcon.Critical)
except Exception as e:
self.show_message("Upload Error", f"Unexpected error: {e}", QSystemTrayIcon.Critical)
finally:
reply.deleteLater()
self.current_reply = None
self.show_message("Upload Error", f"Error parsing response: {e}\nResponse: {response_text}", QSystemTrayIcon.Critical)
def show_success_notification(self, file_path, item_url):
config = get_env_config()
if config.get("play_success_sound", False) or config.get("success_audio_path"):
audio_path = config.get("success_audio_path", "")
volume = config.get("success_audio_volume", 15)
if audio_path:
play_success_sound(audio_path, volume=volume)
if not config.get("enable_notifications", True):
return